#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/common/pywrap", sys.argv)
# Run this with --help to see available options for tracing and debugging
# See https://github.com/cockpit-project/cockpit/blob/main/test/common/testlib.py
# "class Browser" and "class MachineCase" for the available API.

import os
import sys
import time

import testlib
from machine.machine_core import ssh_connection

REGISTRIES_CONF = """
[registries.search]
registries = ['localhost:5000', 'localhost:6000']

[registries.insecure]
registries = ['localhost:5000', 'localhost:6000']
"""

NOT_RUNNING = ["Exited", "Stopped"]

# image names used in tests
IMG_ALPINE = "localhost/test-alpine"
IMG_ALPINE_LATEST = IMG_ALPINE + ":latest"
IMG_BUSYBOX = "localhost/test-busybox"
IMG_BUSYBOX_LATEST = IMG_BUSYBOX + ":latest"
IMG_REGISTRY = "localhost/test-registry"
IMG_REGISTRY_LATEST = IMG_REGISTRY + ":latest"


def docker_version(cls):
    version = cls.execute(False, "docker -v").strip().split(' ')[-1]
    # HACK: handle possible rc versions such as 4.4.0-rc2
    return tuple(int(v.split('-')[0]) for v in version.split('.'))


def showImages(browser):
    if browser.attr("#containers-images button.pf-v5-c-expandable-section__toggle", "aria-expanded") == 'false':
        browser.click("#containers-images button.pf-v5-c-expandable-section__toggle")


def checkImage(browser, name, owner):
    showImages(browser)
    browser.wait_visible("#containers-images table")
    browser.wait_js_func("""(function (first, last) {
        let items = ph_select("#containers-images table tbody");
        for (i = 0; i < items.length; i++)
            if (items[i].innerText.trim().startsWith(first) && items[i].innerText.trim().includes(last))
                return true;
        return false;
        })""", name, owner)


@testlib.nondestructive
class TestApplication(testlib.MachineCase):

    def setUp(self):
        super().setUp()
        m = self.machine
        m.execute("""
            systemctl stop docker.service; systemctl stop containerd.service; systemctl --now enable docker.socket
            # Ensure docker is really stopped, otherwise it keeps the containers/ directory busy
            pkill -e -9 docker || true
            while pgrep docker; do sleep 0.1; done
            pkill -e -9 containerd || true
            while pgrep containerd; do sleep 0.1; done
            findmnt --list -otarget | grep /var/lib/docker/. | xargs -r umount
            sync
            """)

        # backup/restore pristine docker state, so that tests can run on existing testbeds
        self.restore_dir("/var/lib/docker")

        # HACK: sometimes docker leaks mounts
        self.addCleanup(m.execute, """
            systemctl stop docker.service containerd.service docker.socket

            systemctl reset-failed docker.service docker.socket
            docker system reset --force
            pkill -e -9 docker || true
            while pgrep docker; do sleep 0.1; done
            pkill -e -9 containerd || true
            while pgrep containerd; do sleep 0.1; done

            # HACK: sometimes docker leaks mounts
            findmnt --list -otarget | grep /var/lib/docker/. | xargs -r umount
            sync
            """)

        # Create admin session
        m.execute("""
            if [ ! -d /home/admin/.ssh ]; then
                mkdir /home/admin/.ssh
                cp /root/.ssh/* /home/admin/.ssh
                chown -R admin:admin /home/admin/.ssh
                chmod -R go-wx /home/admin/.ssh
            fi
            """)
        self.admin_s = ssh_connection.SSHConnection(user="admin",
                                                    address=m.ssh_address,
                                                    ssh_port=m.ssh_port,
                                                    identity_file=m.identity_file)

        # HACK: system reset has 10s timeout, make that faster with an extra `stop`
        # https://github.com/containers/podman/issues/21874
        # Ubuntu 22.04 has old podman that does not know about rm --time
        if m.image == 'ubuntu-2204':
            self.addCleanup(self.admin_s.execute, "docker rm --force --all", timeout=300)
            self.addCleanup(self.admin_s.execute, "docker pod rm --force --all", timeout=300)
        else:
            self.addCleanup(self.admin_s.execute, "docker rm --force --time 0 --all")
            self.addCleanup(self.admin_s.execute, "docker pod rm --force --time 0 --all")

        # But disable it globally so that "systemctl --user disable" does what we expect
        m.execute("systemctl --global disable docker.socket")

        self.allow_journal_messages("/run.*/docker/docker: couldn't connect.*")
        self.allow_journal_messages(".*/run.*/docker/docker.*Connection reset by peer")

        # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1008249
        self.has_criu = "debian" not in m.image and "ubuntu" not in m.image
        self.has_selinux = "arch" not in m.image and "debian" not in m.image and "ubuntu" not in m.image
        self.has_cgroupsV2 = m.image not in ["centos-8-stream"] and not m.image.startswith('rhel-8')

        self.system_images_count = int(self.execute(True, "docker images -n | wc -l").strip())
        self.user_images_count = int(self.execute(False, "docker images -n | wc -l").strip())

        # allow console.error
        self.allow_browser_errors(
            ".*couldn't search registry \".*\": pinging container registry .*",
            ".*Error occurred while connecting console: cannot resize session: cannot resize container.*",
        )

    def tearDown(self):
        if self.getError():
            # dump container logs for debugging
            for auth in [False, True]:
                print(f"----- {'system' if auth else 'user'} containers -----", file=sys.stderr)
                self.execute(auth, "docker ps -a >&2")
                self.execute(auth, 'for c in $(docker ps -aq); do echo "---- $c ----" >&2; docker logs $c >&2; done')

        super().tearDown()

    def getRestartPolicy(self, auth, container_name):
        cmd = f"docker inspect --format '{{{{.HostConfig.RestartPolicy}}}}' {container_name}"
        return self.execute(auth, cmd).strip()

    def waitNumImages(self, expected):
        self.browser.wait_js_func("ph_count_check", "#containers-images table[aria-label='Images'] > tbody", expected)

    def waitNumContainers(self, expected, auth):
        if auth and self.machine.ostree_image:
            extra = 1  # cockpit/ws
        else:
            extra = 0

        self.browser.wait_js_func("ph_count_check", "#containers-containers tbody", expected + extra)

    def performContainerAction(self, container, cmd):
        b = self.browser
        b.click(f"#containers-containers tbody tr:contains('{container}') .pf-v5-c-menu-toggle")
        b.click(f"#containers-containers tbody tr:contains('{container}') button.pf-v5-c-menu__item:contains({cmd})")

    def getContainerAction(self, container, cmd):
        return f"#containers-containers tbody tr:contains('{container}') button.pf-v5-c-menu__item:contains({cmd})"

    def toggleExpandedContainer(self, container):
        b = self.browser
        b.click(f"#containers-containers tbody tr:contains('{container}') .pf-v5-c-table__toggle button")

    def getContainerAttr(self, container, key, selector=""):
        b = self.browser
        return b.text(f"#containers-containers tbody tr:contains('{container}') > td[data-label={key}] {selector}")

    def execute(self, system, cmd):
        if system:
            return self.machine.execute(cmd)
        else:
            return self.admin_s.execute(cmd)

    def login(self, system=True):
        # HACK: The first rootless call often gets stuck or fails
        # In such case we have alert banner to start the service (or just empty state)
        # A real user would just hit the button so lets do the same as this is always getting
        # back to us and we waste too much time reporting to docker with mixed results.
        # Examples:
        #     https://github.com/containers/docker/issues/8762
        #     https://github.com/containers/docker/issues/9251
        #     https://github.com/containers/docker/issues/6660

        b = self.browser

        self.login_and_go("/docker", superuser=system)
        b.wait_visible("#app")

        with self.browser.wait_timeout(30):
            try:
                b.wait_not_in_text("#containers-containers", "Loading")
                b.wait_not_present("#overview div.pf-v5-c-alert")
            except testlib.Error:
                if system:
                    b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)")
                    b.wait_not_present("#overview div.pf-v5-c-alert")
                else:
                    b.click("#app .pf-v5-c-empty-state button.pf-m-primary")
                    b.wait_not_present("#app .pf-v5-c-empty-state button")

    def waitPodRow(self, podName, present=False):
        if present:
            self.browser.wait_visible("#table-" + podName)
        else:
            self.browser.wait_not_present("#table-" + podName)

    def waitPodContainer(self, podName, containerList, system=True):
        if len(containerList):
            for container in containerList:
                self.waitContainer(container["id"], system, name=container["name"], image=container["image"],
                                   cmd=container["command"], state=container["state"], pod=podName)
        else:
            if self.browser.val("#containers-containers-filter") == "all":
                self.browser.wait_in_text("#table-" + podName + " .pf-v5-c-empty-state", "No containers in this pod")
            else:
                self.browser.wait_in_text("#table-" + podName + " .pf-v5-c-empty-state",
                                          "No running containers in this pod")

    def waitContainerRow(self, container, present=True):
        b = self.browser
        if present:
            b.wait_visible(f'#containers-containers td[data-label="Container"]:contains("{container}")')
        else:
            b.wait_not_present(f'#containers-containers td[data-label="Container"]:contains("{container}")')

    def performPodAction(self, podName, podOwner, action):
        b = self.browser

        b.click(f"#pod-{podName}-{podOwner}-action-toggle")
        b.click(f"ul.pf-v5-c-menu__list li > button.pod-action-{action.lower()}")
        b.wait_not_present("ul.pf-v5-c-menu__list")

    def getStartTime(self, container: str, *, auth: bool) -> str:
        # don't format the raw time strings from the API, force json format
        out = self.execute(auth, "docker inspect --format '{{json .State.StartedAt}}' " + container)
        return out.strip().replace('"', '')

    def waitRestart(self, container: str, old_start: str, *, auth: bool) -> int:
        for _ in range(10):
            new_start = self.getStartTime(container, auth=auth)
            if new_start > old_start:
                return new_start
            time.sleep(1)
        else:
            self.fail("Timed out waiting for StartedAt change")

    # def testPods(self):
    #     b = self.browser

    #     self.login()

    #     self.filter_containers("running")
    #     if not self.machine.ostree_image:
    #         b.wait_in_text("#containers-containers", "No running containers")

    #     # Run a pods as system
    #     self.machine.execute("docker pod create --infra=false --name pod-1")

    #     self.waitPodRow("pod-1", False)
    #     self.filter_containers("all")
    #     self.waitPodContainer("pod-1", [])

    #     def get_pod_cpu_usage(pod_name):
    #         cpu = self.browser.text(f"#table-{pod_name}-title .pod-cpu")
    #         self.assertIn('%', cpu)
    #         return float(cpu[:-1])

        # def get_pod_memory(pod_name):
        #     memory = self.browser.text(f"#table-{pod_name}-title .pod-memory")
        #     try:
        #         value, unit = memory.split(' ')
        #         self.assertIn(unit, ["GB", "MB", "kB", "B"])
        #         return float(value)
        #     except ValueError:
        #         # no unit → only for 0 or not initialized yet
        #         self.assertIn(memory, ["0", ""])
        #         return 0

        # run_cmd = f"docker run -d --pod pod-1 --name test-pod-1-system --stop-timeout 0 {IMG_ALPINE} sleep 100"
        # containerId = self.machine.execute(run_cmd).strip()
        # self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
        #                                  "command": "sleep 100", "state": "Running", "id": containerId}])
        # cpu = get_pod_cpu_usage("pod-1")
        # b.wait(lambda: get_pod_memory("pod-1") > 0)

    #     # Test that cpu usage increases
    #     self.machine.execute("docker exec -di test-pod-1-system sh -c 'dd bs=1024 < /dev/urandom > /dev/null'")
    #     b.wait(lambda: get_pod_cpu_usage("pod-1") > cpu)

        # self.machine.execute("docker pod stop -t0 pod-1")  # disable timeout, so test doesn't wait endlessly
        # self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
        #                                  "command": "sleep 100", "state": NOT_RUNNING, "id": containerId}])
        # self.filter_containers("running")
        # self.waitPodRow("pod-1", False)

        # self.filter_containers("all")
        # b.set_input_text('#containers-filter', 'pod-1')
        # self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
        #                                  "command": "sleep 100", "state": NOT_RUNNING, "id": containerId}])
        # b.set_input_text('#containers-filter', 'test-pod-1-system')
        # self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
        #                                  "command": "sleep 100", "state": NOT_RUNNING, "id": containerId}])
        # # TODO add pixel test when this is again reachable - https://github.com/cockpit-project/bots/issues/2463

        # # Check Pod Actions
        # self.performPodAction("pod-1", "system", "Start")
        # self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
        #                                  "command": "sleep 100", "state": "Running", "id": containerId}])

        # self.performPodAction("pod-1", "system", "Pause")
        # self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
        #                                  "command": "sleep 100", "state": "Paused", "id": containerId}])

        # self.performPodAction("pod-1", "system", "Unpause")
        # self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
        #                                  "command": "sleep 100", "state": "Running", "id": containerId}])

        # self.performPodAction("pod-1", "system", "Stop")
        # self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
        #                                  "command": "sleep 100", "state": NOT_RUNNING, "id": containerId}])

        # self.machine.execute("docker pod start pod-1")
        # self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
        #                                  "command": "sleep 100", "state": "Running", "id": containerId}])

        # old_start = self.getStartTime("test-pod-1-system", auth=True)
        # self.performPodAction("pod-1", "system", "Restart")
        # self.waitRestart("test-pod-1-system", old_start, auth=True)

        # self.performPodAction("pod-1", "system", "Delete")
        # b.click(".pf-v5-c-modal-box button:contains(Delete)")
        # # Alert should be shown, that running pods need to be force deleted.
        # b.wait_in_text(".pf-v5-c-modal-box__body .pf-v5-c-list", "test-pod-1-system")
        # b.click(".pf-v5-c-modal-box button:contains('Force delete')")
        # self.waitPodRow("pod-1", False)

        # HACK: there is some race here which steals the focus from the filter input and selects the page text instead
        # for _ in range(3):
        #     b.focus('#containers-filter')
        #     time.sleep(1)
        #     if b.eval_js('document.activeElement == document.querySelector("#containers-filter")'):
        #         break
        # b.set_input_text('#containers-filter', '')
        # self.machine.execute("podman pod create --infra=false --name pod-2")
        # self.waitPodContainer("pod-2", [])
        # run_cmd = f"podman run -d --pod pod-2 --name test-pod-2-system --stop-timeout 0 {IMG_ALPINE} sleep 100"
        # containerId = self.machine.execute(run_cmd).strip()
        # self.waitPodContainer("pod-2", [{"name": "test-pod-2-system", "image": IMG_ALPINE,
        #                                  "command": "sleep 100", "state": "Running", "id": containerId}])
        # self.machine.execute("podman rm --force -t0 test-pod-2-system")
        # self.waitPodContainer("pod-2", [])
        # self.performPodAction("pod-2", "system", "Delete")
        # b.wait_not_in_text(".pf-v5-c-modal-box__body", "test-pod-2-system")
        # b.click(".pf-v5-c-modal-box button:contains('Delete')")
        # self.waitPodRow("pod-2", False)

    #     # Volumes / mounts
    #     self.machine.execute("docker pod create -p 9999:9999 -v /tmp:/app --name pod-3")
    #     self.machine.execute("docker pod start pod-3")

        # self.waitPodContainer("pod-3", [])
        # # Verify 1 port mapping
        # b.wait_in_text("#table-pod-3-title .pod-details-ports-btn", "1")
        # b.click("#table-pod-3-title .pod-details-ports-btn")
        # b.wait_in_text(".pf-v5-c-popover__content", "0.0.0.0:9999 → 9999/tcp")
        # # Verify 1 mount
        # b.wait_in_text("#table-pod-3-title .pod-details-volumes-btn", "1")
        # b.click("#table-pod-3-title .pod-details-volumes-btn")
        # b.wait_in_text(".pf-v5-c-popover__content", "/tmp ↔ /app")

    def testBasicSystem(self):
        self._testBasic(True)

        b = self.browser

        # Test dropping and gaining privileges
        b.set_val("#containers-containers-owner", "all")
        self.filter_containers("all")
        self.execute(False, "docker pod create --infra=false --name pod_user")
        self.execute(True, "docker pod create --infra=false --name pod_system")

        checkImage(b, IMG_REGISTRY, "system")
        checkImage(b, IMG_REGISTRY, "admin")
        b.wait_visible("#containers-containers .pod-name:contains('pod_user')")
        b.wait_visible("#containers-containers .pod-name:contains('pod_system')")
        b.wait_visible("#containers-containers .container-name:contains('a')")
        b.wait_visible("#containers-containers .container-name:contains('b')")

        # Drop privileges - all system things should disappear
        b.drop_superuser()
        b.wait_not_present("#containers-containers .pod-name:contains('pod_system')")
        b.wait_not_present("#containers-containers .container-name:contains('a')")
        b.wait_visible("#containers-containers .pod-name:contains('pod_user')")
        b.wait_visible("#containers-containers .container-name:contains('b')")
        # Checking images is harder but if there would be more than one this would fail
        b.wait_visible(f"#containers-images:contains('{IMG_REGISTRY}')")

        # Owner select should disappear
        b.wait_not_present("#containers-containers-owner")

        # Also user selection in image download should not be visible
        b.click("#image-actions-dropdown")
        b.click("button:contains(Download new image)")

        b.wait_visible('div.pf-v5-c-modal-box header:contains("Search for an image")')
        b.wait_visible("div.pf-v5-c-modal-box footer button:contains(Download):disabled")
        b.wait_not_present("#as-user")
        b.click(".pf-v5-c-modal-box button:contains('Cancel')")
        b.wait_not_present('div.pf-v5-c-modal-box header:contains("Search for an image")')

        # Gain privileges
        b.become_superuser(passwordless=self.machine.image == "rhel4edge")

        # We are notified that we can also start the system one
        b.wait_in_text("#overview div.pf-v5-c-alert .pf-v5-c-alert__title", "Docker service is available")
        b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)")
        b.wait_not_present("#overview div.pf-v5-c-alert .pf-v5-c-alert__title")

        checkImage(b, IMG_REGISTRY, "system")
        checkImage(b, IMG_REGISTRY, "admin")
        b.wait_visible("#containers-containers .pod-name:contains('pod_user')")
        b.wait_visible("#containers-containers .pod-name:contains('pod_system')")
        b.wait_visible("#containers-containers .container-name:contains('a')")
        b.wait_visible("#containers-containers .container-name:contains('b')")

        # Owner select should appear
        b.wait_visible("#containers-containers-owner")

        # Also user selection in image download should be visible
        b.click("#image-actions-dropdown")
        b.click("button:contains(Download new image)")
        b.wait_visible('div.pf-v5-c-modal-box header:contains("Search for an image")')
        b.wait_visible("div.pf-v5-c-modal-box footer button:contains(Download):disabled")
        b.wait_visible("#as-user")
        b.click(".pf-v5-c-modal-box button:contains('Cancel')")
        b.wait_not_present('div.pf-v5-c-modal-box header:contains("Search for an image")')

        # Check that when we filter only system stuff an then drop privileges that we show user stuff
        b.set_val("#containers-containers-owner", "system")
        b.wait_not_present("#containers-containers .pod-name:contains('pod_user')")
        b.wait_not_present("#containers-containers .container-name:contains('b')")
        # Checking images is harder but if there would be more than one this would fail
        b.wait_visible(f"#containers-images:contains('{IMG_REGISTRY}')")

        b.drop_superuser()
        b.wait_visible("#containers-containers .pod-name:contains('pod_user')")
        b.wait_visible("#containers-containers .container-name:contains('b')")
        # Checking images is harder but if there would be more than one this would fail
        b.wait_visible(f"#containers-images:contains('{IMG_REGISTRY}')")

        # Check showing of entrypoint
        b.click("#containers-containers-create-container-btn")
        b.click("#create-image-image-select-typeahead")
        b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_REGISTRY}")')
        b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yml')
        b.wait_text("#run-image-dialog-entrypoint", '/entrypoint.sh')

        # Deleting image will cleanup both command and entrypoint
        b.click("button.pf-v5-c-select__toggle-clear")
        b.wait_val("#run-image-dialog-command", '')
        b.wait_not_present("#run-image-dialog-entrypoint")

        # Edited command will not be cleared
        b.click("#create-image-image-select-typeahead")
        b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_REGISTRY}")')
        b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yml')
        b.set_input_text("#run-image-dialog-command", '/etc/docker/registry/config.yaml')
        b.click("button.pf-v5-c-select__toggle-clear")
        b.wait_not_present("#run-image-dialog-entrypoint")
        b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yaml')

        # Setting a new image will still keep the old command and not prefill it
        b.click("#create-image-image-select-typeahead")
        b.click(f'button.pf-v5-c-select__menu-item:contains({IMG_ALPINE})')
        b.wait_visible("#run-image-dialog-pull-latest-image")
        b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yaml')

        b.logout()

        if self.machine.ostree_image:
            self.machine.execute("echo foobar | passwd --stdin root")
            self.write_file("/etc/ssh/sshd_config.d/99-root-password.conf", "PermitRootLogin yes",
                            post_restore_action="systemctl try-restart sshd")
            self.machine.execute("systemctl try-restart sshd")

        # Test that when root is logged in we don't present "user" and "system"
        self.login_and_go("/docker", user="root", enable_root_login=True)
        b.wait_visible("#app")

        # `User Service is also available` banner should not be present
        b.wait_not_present("#overview div.pf-v5-c-alert")
        # There should not be any duplicate images listed
        # The "busybox" and "alpine" images have been deleted by _testBasic.
        showImages(b)
        self.waitNumImages(self.system_images_count - 2)
        # There should not be 'owner' selector
        b.wait_not_present("#containers-containers-owner")

        # Test the isSystem boolean for searching
        # https://github.com/cockpit-project/cockpit-podman/pull/891
        b.click("#containers-containers-create-container-btn")
        b.set_input_text("#create-image-image-select-typeahead", "registry")
        b.wait_visible('button.pf-v5-c-select__menu-item:contains("registry")')

    def testBasicUser(self):
        self._testBasic(False)

    def _testBasic(self, auth):
        b = self.browser

        def clickDeleteImage(image_sel):
            b.click(f'{image_sel} .pf-v5-c-menu-toggle')
            b.click(image_sel + " button.btn-delete")

        if not auth:
            self.allow_browser_errors("Failed to start system docker.socket.*")

        expected_ws = ""
        if auth and self.machine.ostree_image:
            expected_ws += "ws"

        self.login(auth)

        # Check all containers
        if auth:
            checkImage(b, IMG_ALPINE, "system")
            checkImage(b, IMG_BUSYBOX, "system")
            checkImage(b, IMG_REGISTRY, "system")

        checkImage(b, IMG_ALPINE, "admin")
        checkImage(b, IMG_BUSYBOX, "admin")
        checkImage(b, IMG_REGISTRY, "admin")

        # Check order of images
        text = b.text("#containers-images table")
        if auth:
            # all user images before all system images
            self.assertRegex(text, ".*admin.*system.*")
            self.assertNotRegex(text, ".*system.*admin.*")
        else:
            self.assertNotIn("system", text)
        # images are sorted alphabetically
        self.assertRegex(text, ".*/test-alpine.*/test-busybox.*/test-registry")

        # build a dummy image so that the timestamp is "today" (for predictable pixel tests)
        # ensure that CMD neither comes first (docker rmi leaves that layer otherwise)
        # nor last (then the topmost layer does not match the image ID)
        IMG_HELLO_LATEST = "localhost/test-hello:latest"
        self.machine.execute(f"""set -eu; D={self.vm_tmpdir}/hello;
        mkdir $D
        printf 'FROM scratch\\nCOPY test.txt /\\nCMD ["/run.sh"]\\nCOPY test.txt /test2.txt\\n' > $D/Containerfile
        echo hello > $D/test.txt""")
        self.execute(auth, f"docker build -t {IMG_HELLO_LATEST} {self.vm_tmpdir}/hello")

        # prepare image ids - much easier to pick a specific container
        images = {}
        for image in self.execute(auth, "docker images --noheading --no-trunc").strip().split("\n"):
            # <name> <tag> sha256:<sha> <other things>
            items = image.split()
            images[f"{items[0]}:{items[1]}"] = items[2].split(":")[-1]

        # show image listing toggle
        hello_sel = f"#containers-images tbody tr[data-row-id=\"{images[IMG_HELLO_LATEST]}{auth}\"]".lower()
        b.wait_visible(hello_sel)
        b.click(hello_sel + " td.pf-v5-c-table__toggle button")
        b.click(hello_sel + " .pf-v5-c-menu-toggle")
        b.wait_visible(hello_sel + " button.btn-delete")
        b.wait_in_text("#containers-images tbody.pf-m-expanded tr .image-details:first-child", "Command/run.sh")
        # Show history
        b.click("#containers-images tbody.pf-m-expanded .pf-v5-c-tabs__list li:nth-child(2) button")
        first_row_sel = "#containers-images .pf-v5-c-table__expandable-row.pf-m-expanded tbody:first-of-type"
        b.wait_in_text(f"{first_row_sel} td[data-label=\"ID\"]",
                       images[IMG_HELLO_LATEST][:12])
        created_sel = f"{first_row_sel} td[data-label=\"Created\"]"
        b.wait_in_text(f"{created_sel}", "today at")
        # topmost (last) layer
        created_sel = f"{first_row_sel} td[data-label=\"Created by\"]"
        b.wait_in_text(f"{created_sel}", "COPY")
        b.wait_in_text(f"{created_sel}", "in /test2.txt")
        # initial (first) layer
        last_row_sel = "#containers-images .pf-v5-c-table__expandable-row.pf-m-expanded tbody:last-of-type"
        b.wait_in_text(f"{last_row_sel} td[data-label=\"Created by\"]", "COPY")

        self.execute(auth, f"docker rmi {IMG_HELLO_LATEST}")
        b.wait_not_present(hello_sel)

        # make sure no running containers shown; on CoreOS there's the cockpit/ws container
        self.filter_containers('running')
        if auth and self.machine.ostree_image:
            self.waitContainerRow("ws")
        else:
            b.wait_in_text("#containers-containers", "No running containers")

        if auth:
            # Run two containers as system (first exits immediately)
            self.execute(auth, f"docker run -d --name test-sh-system --stop-timeout 0 {IMG_ALPINE} sh")
            self.execute(auth, f"docker run -d --name swamped-crate-system --stop-timeout 0 {IMG_BUSYBOX} sleep 1000")

        # Test owner filtering
        if auth:
            self.waitNumImages(self.user_images_count + self.system_images_count)
            self.waitNumContainers(2, True)

            def verify_system():
                self.waitNumImages(self.system_images_count)
                b.wait_in_text("#containers-images", "system")
                self.waitNumContainers(1, True)
                b.wait_in_text("#containers-containers", "system")

            b.set_val("#containers-containers-owner", "system")
            verify_system()
            b.set_val("#containers-containers-owner", "all")
            b.go("#/?owner=system")
            verify_system()

            def verify_user():
                self.waitNumImages(self.user_images_count)
                b.wait_in_text("#containers-images", "admin")
                self.waitNumContainers(1, False)
                b.wait_in_text("#containers-containers", "admin")

            b.set_val("#containers-containers-owner", "user")
            verify_user()
            b.set_val("#containers-containers-owner", "all")
            b.go("#/?owner=user")
            verify_user()

            b.set_val("#containers-containers-owner", "all")
            self.waitNumImages(self.user_images_count + self.system_images_count)
            self.waitNumContainers(2, True)
        else:  # No 'owner' selector when not privileged
            b.wait_not_present("#containers-containers-owner")

        system_containers = {}
        for container in self.execute(True, "docker ps --all --no-trunc").strip().split("\n")[1:]:
            # <sha> <other things> <name>
            items = container.split()
            system_containers[items[-1]] = items[0]
        # running busybox shown
        if auth:
            self.waitContainerRow("swamped-crate-system")
            self.waitContainer(system_containers["swamped-crate-system"], True, name='swamped-crate-system',
                               image=IMG_BUSYBOX, cmd="sleep 1000", state='Running')

        # exited alpine not shown
        b.wait_not_in_text("#containers-containers", "alpine")

        # show all containers and check status
        b.go("#/?container=all")

        # exited alpine under everything list
        b.wait_visible("#containers-containers")
        if auth:
            self.waitContainer(system_containers["test-sh-system"], True, name='test-sh-system', image=IMG_ALPINE,
                               cmd='sh', state=NOT_RUNNING)

        if auth:
            self.performContainerAction("swamped-crate-system", "Delete")
            self.confirm_modal("Cancel")

        # Checked order of containers
        expected = ["swamped-crate-user"]
        if auth:
            expected.extend(["swamped-crate-system", "test-sh-system"])
        expected.extend([expected_ws])
        b.wait_collected_text("#containers-containers .container-name", ''.join(sorted(expected)))

        # show running container
        self.filter_containers('running')
        if auth:
            self.waitContainer(system_containers["swamped-crate-system"], True, name='swamped-crate-system',
                               image=IMG_BUSYBOX, cmd="sleep 1000", state='Running')
        # check exited alpine not in running list
        b.wait_not_in_text("#containers-containers", "alpine")

        # delete running container busybox using force delete
        if auth:
            self.performContainerAction("swamped-crate-system", "Delete")
            self.confirm_modal("Force delete")
            self.waitContainerRow("swamped-crate-system", False)

        self.filter_containers("all")

        if auth:
            self.waitContainerRow("test-sh-system")
            self.performContainerAction("test-sh-system", "Delete")
            self.confirm_modal("Delete")
            b.wait_not_in_text("#containers-containers", "test-sh-system")

        # delete image busybox that hasn't been used
        # First try to just untag and then remove with more tags
        self.execute(auth, f"docker tag {IMG_BUSYBOX} {IMG_BUSYBOX}:1")
        self.execute(auth, f"docker tag {IMG_BUSYBOX} {IMG_BUSYBOX}:2")
        self.execute(auth, f"docker tag {IMG_BUSYBOX} {IMG_BUSYBOX}:3")
        self.execute(auth, f"docker tag {IMG_BUSYBOX} {IMG_BUSYBOX}:4")

        busybox_sel = f"#containers-images tbody tr[data-row-id=\"{images[IMG_BUSYBOX_LATEST]}{auth}\"]".lower()
        b.click(busybox_sel + " td.pf-v5-c-table__toggle button")

        b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:1")
        b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:2")
        b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:3")
        b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:4")

        clickDeleteImage(busybox_sel)
        self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX_LATEST}']"))
        b.set_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:1']", True)
        b.set_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:3']", True)
        b.set_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX_LATEST}']", False)
        self.confirm_modal("Delete")
        b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX_LATEST}")
        b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:2")
        b.wait_not_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:1")
        b.wait_not_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:3")

        clickDeleteImage(busybox_sel)
        b.click("#delete-all")
        self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX_LATEST}']"))
        self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:2']"))
        self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:4']"))
        self.confirm_modal("Delete")
        self.confirm_modal("Force delete")
        b.wait_not_present(busybox_sel)

        # Check that we correctly show networking information
        # Rootless don't have this info
        if auth:
            self.execute(auth, f"docker run -dt --name net_check --stop-timeout 0 {IMG_ALPINE}")
            self.toggleExpandedContainer("net_check")
            b.wait_in_text(".pf-m-expanded .container-details-networking",
                           self.execute(auth, """
                           docker inspect --format '{{.NetworkSettings.Gateway}}' net_check""").strip())
            b.wait_in_text(".pf-m-expanded .container-details-networking",
                           self.execute(auth, """
                           docker inspect --format '{{.NetworkSettings.IPAddress}}' net_check""").strip())
            b.wait_in_text(".pf-m-expanded .container-details-networking",
                           self.execute(auth, """
                           docker inspect --format '{{.NetworkSettings.MacAddress}}' net_check""").strip())
            self.execute(auth, "docker stop net_check")
            b.wait(lambda: self.execute(True, "docker ps --all | grep -e net_check -e Exited"))
            self.toggleExpandedContainer("net_check")
            sha = self.execute(auth, "docker inspect --format '{{.Id}}' net_check").strip()
            self.waitContainer(sha, auth, state='Exited')

        # delete image alpine that has been used by a container
        self.execute(auth, f"docker run -d --name test-sh4 --stop-timeout 0 {IMG_ALPINE} sh")
        # our pixel test expects both containers to be in state "Exited"
        sha = self.execute(auth, "docker inspect --format '{{.Id}}' test-sh4").strip()
        self.waitContainer(sha, auth, name="test-sh4", state='Exited')
        if auth:
            b.assert_pixels('#app', "overview", ignore=[".ignore-pixels"], skip_layouts=["rtl", "mobile"])
        alpine_sel = f"#containers-images tbody tr[data-row-id=\"{images[IMG_ALPINE_LATEST]}{auth}\"]".lower()
        b.wait_visible(alpine_sel)
        b.click(alpine_sel + " td.pf-v5-c-table__toggle button")
        clickDeleteImage(alpine_sel)
        self.confirm_modal("Delete")
        self.confirm_modal("Force delete")
        b.wait_not_present(alpine_sel)

        b.wait_collected_text("#containers-containers .container-name", expected_ws)
        self.execute(auth, f"docker run -d --name c --stop-timeout 0 {IMG_REGISTRY} sh")
        b.wait_collected_text("#containers-containers .container-name", "c" + expected_ws)
        self.execute(auth, f"docker run -d --name a --stop-timeout 0 {IMG_REGISTRY} sh")
        b.wait_collected_text("#containers-containers .container-name", "ac" + expected_ws)

        self.execute(False, f"docker run -d --name b --stop-timeout 0 {IMG_REGISTRY} sh")
        if auth:
            b.wait_collected_text("#containers-containers .container-name", "abc" + expected_ws)
            self.execute(False, f"docker run -d --name doremi --stop-timeout 0 {IMG_REGISTRY} sh")
            b.wait_collected_text("#containers-containers .container-name", "abcdoremi" + expected_ws)
            b.wait(lambda: self.getContainerAttr("doremi", "State") in NOT_RUNNING)
        else:
            b.wait_collected_text("#containers-containers .container-name", "abc")

        # Test intermediate images
        b.wait_not_present(".listing-action")
        tmpdir = self.execute(auth, "mktemp -d").strip()
        self.execute(auth, f"echo 'FROM {IMG_REGISTRY}\nRUN ls' > {tmpdir}/Dockerfile")
        self.execute(auth, f"docker build {tmpdir}")

        b.wait_not_in_text("#containers-images", "<none>:<none>")
        b.click(".listing-action button:contains('Show intermediate images')")
        b.wait_in_text("#containers-images", "<none>:<none>")
        b.wait_in_text("#containers-images tbody:last-child td[data-label=Created]", "today at")

        b.click(".listing-action button:contains('Hide intermediate images')")
        b.wait_not_in_text("#containers-images", "<none>:<none>")

        # Intermediate images are not shown in create container dialog
        b.click("#containers-containers-create-container-btn")
        b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')
        b.click("#create-image-image-select-typeahead")
        b.wait_visible(f".pf-v5-c-select__menu-item:contains('{IMG_REGISTRY}')")
        b.wait_not_present(".pf-v5-c-select__menu-item:contains('none')")
        b.click(".pf-v5-c-modal-box .btn-cancel")
        b.wait_not_present(".pf-v5-c-modal-box")

        # Delete intermediate images
        intermediate_image_sel = "#containers-images tbody:last-child:contains('<none>:<none>')"
        b.click(".listing-action button:contains('Show intermediate images')")
        clickDeleteImage(intermediate_image_sel)
        self.confirm_modal("Delete")
        b.wait_not_present(intermediate_image_sel)

        # Create intermediate image and use it in a container
        tmpdir = self.execute(auth, "mktemp -d").strip()
        self.execute(auth, f"echo 'FROM {IMG_REGISTRY}\nRUN ls' > {tmpdir}/Dockerfile")
        IMG_INTERMEDIATE = 'localhost/test-intermediate'
        self.execute(auth, f"docker build -t {IMG_INTERMEDIATE} {tmpdir}")
        b.click(f'#containers-images tbody tr:contains("{IMG_INTERMEDIATE}") .ct-container-create')
        b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')
        b.click("#create-image-create-btn")
        b.wait_not_present("div.pf-v5-c-modal-box")
        self.waitContainerRow(IMG_INTERMEDIATE)

        # Integration tab should not crash with an intermediate image
        self.toggleExpandedContainer(IMG_INTERMEDIATE)
        b.click(".pf-m-expanded button:contains('Integration')")

        # Delete intermediate image which is in use
        self.execute(auth, f"docker untag {IMG_INTERMEDIATE}")
        clickDeleteImage(intermediate_image_sel)
        self.confirm_modal("Delete")
        self.confirm_modal("Force delete")
        b.wait_not_in_text("#containers-images", "<none>:<none>")
        b.wait_not_in_text("#containers-containers", IMG_INTERMEDIATE)

    def testCommitUser(self):
        self._testCommit(False)

    def testCommitSystem(self):
        self._testCommit(True)

    def _testCommit(self, auth):
        b = self.browser
        self.allow_browser_errors("Failed to commit container .* repository name must be lowercase")

        self.login(auth)

        # run a container (will exit immediately) and test the display of commit modal
        self.execute(auth, f"docker run -d --name test-sh0 --stop-timeout 0 {IMG_ALPINE} sh -c 'ls -a'")

        self.filter_containers("all")
        self.waitContainerRow("test-sh0")
        self.toggleExpandedContainer("test-sh0")

        self.performContainerAction("test-sh0", "Commit")
        b.wait_visible(".pf-v5-c-modal-box")

        b.wait_in_text(".pf-v5-c-modal-box__description", "state of the test-sh0 container")

        # Empty name yields warning
        b.click("button:contains(Commit)")
        b.wait_text("#commit-dialog-image-name-helper", "Image name is required")
        b.wait_visible("button:contains(Commit):disabled")
        b.wait_visible("button:contains('Force commit')")
        # Warning should be cleaned when updating name
        b.set_input_text("#commit-dialog-image-name", "foobar")
        b.wait_not_present("button:contains('Force commit')")
        b.wait_not_present("#commit-dialog-image-name-helper")

        # Existing name yields warning
        b.set_input_text("#commit-dialog-image-name", IMG_ALPINE)
        b.click("button:contains(Commit)")
        b.wait_text("#commit-dialog-image-name-helper", "Image name is not unique")
        b.wait_visible("button:contains(Commit):disabled")
        b.wait_visible("button:contains('Force commit')")
        # Warning should be cleaned when updating tag
        b.set_input_text("#commit-dialog-image-tag", "foobar")
        b.wait_not_present("button:contains('Force commit')")
        b.wait_not_present("#commit-dialog-image-name-helper")

        # Check failing commit
        b.set_input_text("#commit-dialog-image-name", "TEST")
        b.click("button:contains(Commit)")
        b.wait_in_text(".pf-v5-c-alert", "Failed to commit container test-sh0")
        b.wait_in_text(".pf-v5-c-alert", "repository name must be lowercase")

        # Test cancel
        self.confirm_modal("Cancel")

        # Force commit empty container
        self.performContainerAction("test-sh0", "Commit")
        b.wait_visible(".pf-v5-c-modal-box")
        # We prefill command
        b.wait_val("#commit-dialog-command", 'sh -c "ls -a"')
        # Test docker format
        b.set_checked("#commit-dialog-docker", True)
        b.click("button:contains(Commit)")
        self.confirm_modal("Force commit")

        # don't use waitNumImages() here, as we want to include anonymous images
        def waitImageCount(expected):
            if auth:
                expected += self.system_images_count

            b.wait_in_text("#containers-images", f"{expected} images")

        waitImageCount(self.user_images_count + 1)
        image_id = self.execute(auth, "docker images --sort created --format '{{.Id}}' | head -n 1").strip()
        manifest_type = self.execute(auth, "docker inspect --format '{{.ManifestType}}' " + image_id).strip()
        cmd = self.execute(auth, "docker inspect --format '{{.Config.Cmd}}' " + image_id).strip()
        self.assertIn("docker.distribution.manifest", manifest_type)
        self.assertEqual("[sh -c ls -a]", cmd)

        # Commit with name, tag, author and edited command
        self.performContainerAction("test-sh0", "Commit")
        b.wait_visible(".pf-v5-c-modal-box")
        b.set_input_text("#commit-dialog-image-name", "newname")
        b.set_input_text("#commit-dialog-image-tag", "24")
        b.set_input_text("#commit-dialog-author", "MM")
        b.set_input_text("#commit-dialog-command", "sh -c 'ps'")

        if auth:
            b.assert_pixels(".pf-v5-c-modal-box", "commit", skip_layouts=["rtl"])

        self.confirm_modal("Commit")

        waitImageCount(self.user_images_count + 2)
        self.assertEqual(self.execute(auth, "docker inspect --format '{{.Author}}' newname:24").strip(), "MM")
        self.assertEqual(self.execute(auth, "docker inspect --format '{{.Config.Cmd}}' newname:24").strip(),
                         "[sh -c ps]")
        self.assertIn("vnd.oci.image.manifest",
                      self.execute(auth, "docker inspect --format '{{.ManifestType}}' newname:24").strip())

        # Test commit of running container
        self.execute(auth, f"docker run -d --name test-sh2 --stop-timeout 0 {IMG_BUSYBOX} sleep 1000")
        self.performContainerAction("test-sh2", "Commit")
        b.wait_visible(".pf-v5-c-modal-box")
        b.set_input_text("#commit-dialog-image-name", "newname")
        self.confirm_modal("Commit")
        waitImageCount(self.user_images_count + 3)
        self.assertEqual(self.execute(auth,
                                      "docker inspect --format '{{.Config.Cmd}}' newname:latest").strip(),
                         "[sleep 1000]")

        # Test commit of running container with pause (also conflicting name through :latest)
        # This only works on rootless with cgroupsv2
        if auth or self.has_cgroupsV2:
            self.performContainerAction("test-sh2", "Commit")
            b.wait_visible(".pf-v5-c-modal-box")
            b.set_input_text("#commit-dialog-image-name", "newname")
            b.set_checked("#commit-dialog-pause", True)
            b.click("button:contains(Commit)")
            self.confirm_modal("Force commit")
            waitImageCount(self.user_images_count + 4)

    def testDownloadImage(self):
        b = self.browser
        execute = self.execute

        def prepare():
            # Create and start registry containers
            self.execute(True, f"docker run -d -p 5000:5000 --name registry --stop-timeout 0 {IMG_REGISTRY}")
            self.execute(True, f"docker run -d -p 6000:5000 --name registry_alt --stop-timeout 0 {IMG_REGISTRY}")
            # Add local insecure registry into registries conf
            self.machine.write("/etc/containers/registries.conf", REGISTRIES_CONF)
            self.execute(True, "systemctl stop docker.service")
            # Push busybox image to the local registries
            self.execute(True,
                         f"docker tag {IMG_BUSYBOX} localhost:5000/my-busybox; docker push localhost:5000/my-busybox")
            self.execute(True,
                         f"docker tag {IMG_BUSYBOX} localhost:6000/my-busybox; docker push localhost:6000/my-busybox")
            # Untag busybox image which duplicates the image we are about to download
            self.execute(True, f"docker rmi -f {IMG_BUSYBOX} localhost:5000/my-busybox localhost:6000/my-busybox")
            self.execute(False, f"docker rmi -f {IMG_BUSYBOX}")

        class DownloadImageDialog():
            def __init__(self, test_obj, imageName, imageTag=None, user="system"):
                self.imageName = imageName
                self.imageTag = imageTag
                self.user = user
                self.imageSha = ""
                self.assertTrue = test_obj.assertTrue

            def openDialog(self):
                # Open get new image modal
                b.click("#image-actions-dropdown")
                b.click("button:contains(Download new image)")
                b.wait_visible('div.pf-v5-c-modal-box header:contains("Search for an image")')
                b.wait_visible("div.pf-v5-c-modal-box footer button:contains(Download):disabled")

                return self

            def fillDialog(self):
                # Search for image specified with self.imageName and self.imageTag
                b.click(f"#{self.user}")
                b.set_val('#registry-select', "localhost:5000")
                # HACK: Sometimes the value is not shown fully. FIXME
                b.set_input_text("#search-image-dialog-name", self.imageName, value_check=False)
                if self.imageTag:
                    b.set_input_text(".image-tag-entry input", self.imageTag)

                return self

            def selectImageAndDownload(self):
                # Select and download the self.imageName image
                b.click(f".pf-v5-c-data-list .image-name:contains({self.imageName})")
                b.click("div.pf-v5-c-modal-box footer button:contains(Download)")
                b.wait_not_present("div.pf-v5-c-modal-box")

                return self

            def expectDownloadErrorForNonExistingTag(self):
                title = f"Danger alert:Failed to download image localhost:5000/{self.imageName}:{self.imageTag}"
                b.wait_visible(f'h4.pf-v5-c-alert__title:contains("{title}")')

                return self

            def expectSearchErrorForNotExistingImage(self):
                b.wait_visible(f".pf-v5-c-modal-box__body:contains(No results for {self.imageName})")
                b.click(".pf-v5-c-modal-box button.btn-cancel")
                b.wait_not_present(".pf-v5-c-modal-box__body")

                return self

            def expectDownloadSuccess(self):
                # Confirm that the modal dialog is not open anymore
                b.wait_not_present('div.pf-v5-c-modal-box')
                # Confirm that the image got downloaded
                checkImage(b,
                           f"localhost:5000/{self.imageName}:{self.imageTag or 'latest'}",
                           "system" if self.user == "system" else "admin")

                # Confirm that no error has happened
                b.wait_not_present('h4.pf-v5-c-alert__title:contains("Failed to download image")')

                # Find out this image ID
                container_name = f"localhost:5000/{self.imageName}:{self.imageTag or 'latest'}"
                self.imageSha = execute(self.user == "system",
                                        f"docker inspect --format '{{{{.Id}}}}' {container_name}").strip()

                return self

            def deleteImage(self, force=False, another=None):
                imageTagSuffix = ":" + (self.imageTag or 'latest')

                # Select the image row

                # show image listing toggle
                imageId = f"{self.imageSha}{'true' if self.user == 'system' else 'false'}"
                sel = f"#containers-images tbody tr[data-row-id=\"{imageId}\"]"
                b.wait_visible(sel)
                b.click(sel + " td.pf-v5-c-table__toggle button")

                # Click the delete icon on the image row
                b.click(sel + " .pf-v5-c-menu-toggle")
                b.click(sel + ' button.btn-delete')

                if another:
                    b.click("#delete-all")
                    sel = f".pf-v5-c-check__input[aria-label='localhost:5000/{self.imageName}{imageTagSuffix}']"
                    self.assertTrue(b.get_checked(sel))
                    self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{another}']"))
                    b.click("#delete-all")
                    b.wait_visible("#btn-img-delete:disabled")

                    b.set_checked(
                        f".pf-v5-c-check__input[aria-label='localhost:5000/{self.imageName}{imageTagSuffix}']", True)
                    b.set_checked(f".pf-v5-c-check__input[aria-label='{another}']", True)

                # Confirm deletion in the delete dialog
                b.click(".pf-v5-c-modal-box #btn-img-delete")

                if force:
                    # Confirm force delete
                    b.click(".pf-v5-c-modal-box button:contains('Force delete')")

                b.wait_not_present(sel)

                return self

        prepare()

        self.login()

        # Test registries
        b.click("#image-actions-dropdown")
        b.click("button:contains(Download new image)")
        b.wait_visible('div.pf-v5-c-modal-box header:contains("Search for an image")')
        # HACK: Sometimes the value is not shown fully. FIXME
        b.set_input_text("#search-image-dialog-name", "my-busybox", value_check=False)

        b.wait_visible(".pf-v5-c-data-list .image-name:contains('localhost:5000/my-busybox')")
        b.wait_visible(".pf-v5-c-data-list .image-name:contains('localhost:6000/my-busybox')")
        b.assert_pixels(".docker-search", "download", skip_layouts=["rtl"])

        b.set_val('#registry-select', "localhost:6000")
        b.wait_not_present(".pf-v5-c-data-list .image-name:contains('localhost:5000/my-busybox')")
        b.wait_visible(".pf-v5-c-data-list .image-name:contains('localhost:6000/my-busybox')")
        b.click(".pf-v5-c-modal-box button:contains('Cancel')")
        b.wait_not_present('div.pf-v5-c-modal-box')

        dialog0 = DownloadImageDialog(self, imageName='my-busybox', user="system")
        dialog0.openDialog() \
            .fillDialog() \
            .selectImageAndDownload() \
            .expectDownloadSuccess()
        dialog0.deleteImage()

        dialog1 = DownloadImageDialog(self, imageName='my-busybox', user="user")
        dialog1.openDialog() \
            .fillDialog() \
            .selectImageAndDownload() \
            .expectDownloadSuccess()
        # test recognition/deletion of multiple image tags
        second_tag = "localhost/copybox:latest"
        self.execute(False, f"docker tag localhost:5000/my-busybox {second_tag}")
        # expand details
        b.click("#containers-images tr:contains('my-busybox') td.pf-v5-c-table__toggle button")
        b.wait_in_text("#containers-images tbody.pf-m-expanded tr .image-details", second_tag)
        dialog1.deleteImage(True, another=second_tag)

        dialog = DownloadImageDialog(self, imageName='my-busybox', imageTag='latest', user="system")
        dialog.openDialog() \
              .fillDialog() \
              .selectImageAndDownload() \
              .expectDownloadSuccess() \
              .deleteImage()

        dialog = DownloadImageDialog(self, imageName='foobar')
        dialog.openDialog() \
              .fillDialog() \
              .expectSearchErrorForNotExistingImage()

        dialog = DownloadImageDialog(self, imageName='my-busybox', imageTag='foobar')
        dialog.openDialog() \
              .fillDialog() \
              .selectImageAndDownload() \
              .expectDownloadErrorForNonExistingTag()

    def testLifecycleOperationsUser(self):
        self._testLifecycleOperations(False)

    def testLifecycleOperationsSystem(self):
        self._testLifecycleOperations(True)

    def _testLifecycleOperations(self, auth):
        b = self.browser

        if not auth:
            self.allow_browser_errors("Failed to start system docker.socket.*")

        self.login()
        self.filter_containers('all')

        # run a container
        self.execute(auth, f"""
                docker run -d --name swamped-crate --stop-timeout 0 {IMG_BUSYBOX} sh -c 'echo 123; sleep infinity';
                docker stop swamped-crate""")
        b.wait(lambda: self.execute(auth, "docker ps --all | grep -e swamped-crate -e Exited"))

        b.wait_visible("#containers-containers")
        container_sha = self.execute(auth, "docker inspect --format '{{.Id}}' swamped-crate").strip()
        self.waitContainer(container_sha, auth, name='swamped-crate', image=IMG_BUSYBOX,
                           state='Exited', owner="system" if auth else "admin")
        b.click("#containers-containers tbody tr:contains('swamped-crate') .pf-v5-c-menu-toggle")

        if not auth:
            # Checkpoint/restore is not supported on user containers yet - the related buttons should not be shown
            # Check that the restore option is not present
            b.wait_not_present(self.getContainerAction('swamped-crate', 'Restore'))

        # Health check is not set up
        b.wait_not_present(self.getContainerAction('swamped-crate', 'Run health check'))

        b.click("#containers-containers tbody tr:contains('swamped-crate') .pf-v5-c-menu-toggle")

        # Start the container
        self.performContainerAction(IMG_BUSYBOX, "Start")

        self.waitContainer(container_sha, auth, name='swamped-crate', image=IMG_BUSYBOX,
                           state='Running', owner="system" if auth else "admin")

        def get_cpu_usage(sel):
            cpu = self.getContainerAttr(sel, "CPU")
            self.assertIn('%', cpu)
            # If it not a number it will raise ValueError which is what we want to know
            return float(cpu[:-1])

        # Check we show usage
        b.wait(lambda: self.getContainerAttr(IMG_BUSYBOX, "CPU") != "")
        b.wait(lambda: self.getContainerAttr(IMG_BUSYBOX, "Memory") != "")
        memory = self.getContainerAttr(IMG_BUSYBOX, "Memory")
        if auth or self.has_cgroupsV2:
            cpu = get_cpu_usage(IMG_BUSYBOX)

            self.assertIn('/', memory)
            numbers = memory.split('/')
            self.assertTrue(numbers[0].strip().replace('.', '', 1).isdigit())
            full = numbers[1].strip().split()
            self.assertTrue(full[0].replace('.', '', 1).isdigit())
            self.assertIn(full[1], ["GB", "MB"])

            # Test that the value is updated dynamically
            self.execute(auth, "docker exec -i swamped-crate sh -c 'dd bs=1024 < /dev/urandom > /dev/null &'")
            b.wait(lambda: get_cpu_usage(IMG_BUSYBOX) > cpu)
            self.execute(auth, "docker exec swamped-crate sh -c 'pkill dd'")
        else:
            # No support for CGroupsV2
            self.assertEqual(self.getContainerAttr(IMG_BUSYBOX, "CPU"), "n/a")
            self.assertEqual(memory, "n/a")

        # Restart the container; there is no steady state change in the visible UI, so look for
        # a changed data-started-at attribute
        old_start = self.getStartTime("swamped-crate", auth=auth)
        b.wait_in_text(f'#containers-containers tr[data-started-at="{old_start}"]', "swamped-crate")
        self.performContainerAction(IMG_BUSYBOX, "Force restart")
        new_start = self.waitRestart("swamped-crate", old_start, auth=auth)
        b.wait_in_text(f'#containers-containers tr[data-started-at="{new_start}"]', "swamped-crate")
        self.waitContainer(container_sha, auth, name='swamped-crate', image=IMG_BUSYBOX, state='Running')

        self.waitContainerRow(IMG_BUSYBOX)
        if not auth:
            # Check that the checkpoint option is not present for rootless
            b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle")
            b.wait_visible(self.getContainerAction(IMG_BUSYBOX, 'Force stop'))
            b.wait_not_present(self.getContainerAction(IMG_BUSYBOX, 'Checkpoint'))
            b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle")
        # Stop the container
        self.performContainerAction(IMG_BUSYBOX, "Force stop")

        self.waitContainer(container_sha, auth, name='swamped-crate', image=IMG_BUSYBOX)
        b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING)
        b.wait(lambda: self.getContainerAttr("swamped-crate", "CPU") == "")
        b.wait(lambda: self.getContainerAttr("swamped-crate", "Memory") == "")

        # Check that container details are not lost when the container is stopped
        self.toggleExpandedContainer("swamped-crate")
        b.click(".pf-m-expanded button:contains('Integration')")
        b.wait_visible(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Environment variables")')

        # Check that console reconnects when container starts
        b.click(".pf-m-expanded button:contains('Console')")
        b.wait_text(".pf-m-expanded .pf-v5-c-empty-state", "Container is not running")
        self.performContainerAction("swamped-crate", "Start")
        b.wait_in_text(".pf-m-expanded .xterm-accessibility-tree", "/ # ")
        b.focus(".pf-m-expanded .xterm-helper-textarea")
        b.key_press('clear\r')
        b.wait_not_in_text(".pf-m-expanded .xterm-accessibility-tree", "clear")
        b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(1)", "/ # ")
        b.key_press('echo hello\r')
        b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(2)", "hello")
        b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(3)", "/ # ")
        self.performContainerAction("swamped-crate", "Stop")
        b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(3)", "/ #  disconnected ")
        sha = self.execute(auth, "docker inspect --format '{{.Id}}' swamped-crate").strip()
        self.waitContainer(sha, auth, name='swamped-crate', image=IMG_BUSYBOX, state=NOT_RUNNING)
        self.performContainerAction("swamped-crate", "Start")
        self.waitContainer(sha, auth, state='Running')
        b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(1)", "/ # ")
        b.wait_not_in_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(2)", "hello")

        # Check that logs reconnect when container starts
        b.click(".pf-m-expanded button:contains('Logs')")
        self.performContainerAction("swamped-crate", "Stop")
        self.waitContainer(sha, auth, state=NOT_RUNNING)
        b.wait_in_text(".pf-m-expanded .container-logs .xterm-accessibility-tree", "Streaming disconnected")
        self.performContainerAction("swamped-crate", "Start")
        b.wait_in_text(".pf-m-expanded .container-logs .xterm-accessibility-tree", "Streaming disconnected123")

    def testCheckpointRestore(self):
        m = self.machine
        b = self.browser

        self.login()
        self.filter_containers('all')

        if not self.has_criu:
            # On cgroupsv1 systems just check that we get expected error messages

            # Run a container
            self.execute(True, f"docker run -dit --name swamped-crate --stop-timeout 0 {IMG_BUSYBOX} sh")
            b.wait(lambda: self.execute(True, "docker ps --all | grep -e swamped-crate"))

            # Checkpoint the container
            self.performContainerAction(IMG_BUSYBOX, "Checkpoint")
            b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-keep', True)
            b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-tcpEstablished', True)
            b.click('.pf-v5-c-modal-box button:contains(Checkpoint)')
            b.wait_not_present('.modal_dialog')

            def criu_alert():
                text = b.text(".pf-v5-c-alert.pf-m-danger > .pf-v5-c-alert__description").lower()
                return "checkpoint/restore requires at least criu" in text or "failed to check for criu" in text
            b.wait(criu_alert)
            return

        # Run a container
        mac_address = '92:d0:c6:0a:29:38'
        self.execute(True, f"""
            docker run -dit --mac-address {mac_address} --name swamped-crate --stop-timeout 0 {IMG_BUSYBOX} sh;
            docker stop swamped-crate
        """)
        b.wait(lambda: self.execute(True, "docker ps --all | grep -e swamped-crate -e Exited"))

        # Check that the restore option is not present (i.e. start is a regular button)
        b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle")
        b.wait_not_present(self.getContainerAction(IMG_BUSYBOX, 'Restore'))
        b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle")

        # Start the container
        self.performContainerAction("swamped-crate", "Start")
        b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in 'Running')

        self.toggleExpandedContainer("swamped-crate")
        b.wait_visible(".pf-m-expanded button:contains('Details')")
        b.wait_not_present(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Latest checkpoint")')

        # Checkpoint the container
        self.performContainerAction("swamped-crate", "Checkpoint")
        b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-keep', True)
        b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-tcpEstablished', True)
        b.click('.pf-v5-c-modal-box button:contains(Checkpoint)')

        with b.wait_timeout(300):
            b.wait_not_present(".pf-v5-c-modal-box")

        if self.has_criu:
            b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING)
            b.wait_in_text(
                f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Latest checkpoint") + dd',
                'today at'
            )
        else:
            # expect proper error message
            b.wait_in_text(".pf-v5-c-alert.pf-m-danger", "Failed to checkpoint container swamped-crate")
            b.wait(lambda: "checkpoint/restore requires at least criu" in
                   b.text(".pf-v5-c-alert.pf-m-danger > .pf-v5-c-alert__description").lower())
            return

        # Restore the container
        self.waitContainerRow("swamped-crate")
        self.performContainerAction("swamped-crate", "Restore")
        b.set_checked('.pf-v5-c-modal-box input#restore-dialog-keep', True)
        b.set_checked('.pf-v5-c-modal-box input#restore-dialog-tcpEstablished', True)
        b.set_checked('.pf-v5-c-modal-box input#restore-dialog-ignoreStaticIP', True)
        b.set_checked('.pf-v5-c-modal-box input#restore-dialog-ignoreStaticMAC', True)
        b.click('.pf-v5-c-modal-box button:contains(Restore)')
        b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in 'Running')

        # A new MAC address should have been generated
        # Fixed in docker 4.4.0 https://github.com/containers/docker/issues/16666
        cmd = "docker inspect --format '{{.NetworkSettings.MacAddress}}' swamped-crate"
        new_mac_address = self.execute(True, cmd).strip()
        if docker_version(self) >= (4, 4, 0):
            self.assertNotEqual(new_mac_address, mac_address)
        else:
            self.assertEqual(new_mac_address, mac_address)

        # Checkpoint the container without stopping
        self.waitContainerRow("swamped-crate")
        self.performContainerAction("swamped-crate", "Checkpoint")
        b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-leaveRunning', True)
        b.click('.pf-v5-c-modal-box button:contains(Checkpoint)')
        b.wait_not_present('.modal_dialog')

        # Stop the container
        m.execute("docker stop swamped-crate")
        b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING)

        # Restore the container
        self.performContainerAction("swamped-crate", "Restore")
        b.click('.pf-v5-c-modal-box button:contains(Restore)')
        b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in 'Running')

    def testNotRunning(self):
        b = self.browser

        def disable_system():
            self.execute(True, "systemctl disable --now docker.socket; systemctl stop docker.service; systemctl stop containerd.service")

        def enable_system():
            self.execute(True, "systemctl enable --now docker.socket")

        def is_active_system(string):
            b.wait(lambda: self.execute(True, "systemctl is-active docker.socket || true").strip() == string)

        def is_enabled_system(string):
            b.wait(lambda: self.execute(True, "systemctl is-enabled docker.socket || true").strip() == string)

        disable_system()
        self.login_and_go("/docker")

        # Troubleshoot action
        b.click("#app .pf-v5-c-empty-state button.pf-m-link")
        b.enter_page("/system/services")
        # services page is too slow
        with b.wait_timeout(60):
            b.wait_in_text("#service-details", "docker.socket")

        # Start action, with enabling (by default)
        b.go("/docker")
        b.enter_page("/docker")
        b.click("#app .pf-v5-c-empty-state button.pf-m-primary")

        b.wait_visible("#containers-containers")
        b.wait_not_present("#overview div.pf-v5-c-alert.pf-m-info")

        is_active_system("active")
        is_enabled_system("enabled")

        # Start action, without enabling
        disable_system()
        b.click("#app .pf-v5-c-empty-state input[type=checkbox]")
        b.assert_pixels("#app .pf-v5-c-empty-state", "docker-service-disabled", skip_layouts=["medium", "mobile"])
        b.click("#app .pf-v5-c-empty-state button.pf-m-primary")

        b.wait_visible("#containers-containers")
        is_enabled_system("disabled")
        is_active_system("active")

        b.logout()
        disable_system()
        # HACK: Due to https://github.com/containers/docker/issues/7180, avoid
        # user docker.service to time out; make sure to start it afresh
        self.login_and_go("/docker")
        self.login_and_go("/docker")
        b.wait_in_text("#overview div.pf-v5-c-alert .pf-v5-c-alert__title", "Docker service is available")
        b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)")
        b.wait_not_present("#overview div.pf-v5-c-alert")
        is_active_system("active")
        is_enabled_system("enabled")

        b.logout()
        disable_system()
        self.login_and_go("/docker", superuser=False)
        b.click("#app .pf-v5-c-empty-state button.pf-m-primary")
        b.wait_visible("#containers-containers")
        b.wait_not_present("#overview div.pf-v5-c-alert")

        is_active_system("inactive")
        is_enabled_system("disabled")
        b.logout()

        # no Troubleshoot action without cockpit-system package
        disable_system()
        self.restore_dir("/usr/share/cockpit/systemd")
        self.machine.execute("rm /usr/share/cockpit/systemd/manifest.json")
        self.login_and_go("/docker")
        b.wait_visible("#app .pf-v5-c-empty-state button.pf-m-primary")
        self.assertFalse(b.is_present("#app .pf-v5-c-empty-state button.pf-m-link"))
        # starting still works
        b.click("#app .pf-v5-c-empty-state button.pf-m-primary")
        b.wait_visible("#containers-containers")

        self.allow_restart_journal_messages()
        self.allow_journal_messages(".*docker/docker.sock/.*: couldn't connect:.*")
        self.allow_journal_messages(".*docker/docker.sock: .*Connection.*Error.*")
        self.allow_journal_messages(".*docker/docker.sock/.*/events.*: received truncated HTTP response.*")

    def testCreateContainerSystem(self):
        self._testCreateContainer(True)

    def testCreateContainerUser(self):
        self._testCreateContainer(False)

    def _testCreateContainer(self, auth):
        new_container = 'new-container'
        self.execute(True, f"docker run -d --name {new_container} --stop-timeout 0 {IMG_BUSYBOX} touch /latest")
        self.execute(True, f"docker commit {new_container} newimage")
        new_image_sha = self.execute(True, "docker inspect --format '{{.Id}}' newimage").strip()

        self.execute(True, f"docker run -d -p 5000:5000 --name registry --stop-timeout 0 {IMG_REGISTRY}")
        self.execute(True, f"docker run -d -p 6000:5000 --name registry_alt --stop-timeout 0 {IMG_REGISTRY}")
        # Add local insecure registry into registries conf
        self.machine.write("/etc/containers/registries.conf", REGISTRIES_CONF)
        self.execute(True, "systemctl stop docker.service")
        # Push busybox image to the local registries
        self.execute(True,
                     f"docker tag {IMG_BUSYBOX} localhost:5000/my-busybox; docker push localhost:5000/my-busybox")
        self.execute(True,
                     f"docker tag {IMG_BUSYBOX} localhost:6000/my-busybox; docker push localhost:6000/my-busybox")
        # Untag busybox image which duplicates the image we are about to download
        self.execute(True, f"docker rmi -f {IMG_BUSYBOX} localhost:5000/my-busybox localhost:6000/my-busybox")

        self.login(auth)

        b = self.browser
        container_name = "busybox-downloaded"

        b.click("#containers-containers button.pf-v5-c-button.pf-m-primary")
        b.set_input_text("#run-image-dialog-name", container_name)

        # Test invalid input
        b.set_input_text("#create-image-image-select-typeahead", "|alpi*ne?\\")
        b.wait_text("button.pf-v5-c-select__menu-item:not(.pf-m-disabled)", "localhost/test-alpine:latest")

        # No local results found
        b.set_input_text("#create-image-image-select-typeahead", "notfound")

        b.click('button.pf-v5-c-toggle-group__button:contains("Local")')
        b.wait_text("button.pf-v5-c-select__menu-item.pf-m-disabled", "No images found")

        # Local results found
        b.set_input_text("#create-image-image-select-typeahead", "registry")
        if auth:
            b.assert_pixels(".pf-v5-c-modal-box", "image-select", skip_layouts=["rtl"])
        b.click('button.pf-v5-c-toggle-group__button:contains("Local")')
        b.wait_text("button.pf-v5-c-select__menu-item", IMG_REGISTRY_LATEST)

        # Local registry
        b.set_input_text("#create-image-image-select-typeahead", "my-busybox")
        b.click('button.pf-v5-c-toggle-group__button:contains("localhost:5000")')
        b.wait_text("button.pf-v5-c-select__menu-item:not(.pf-m-disabled)", "localhost:5000/my-busybox")

        # Select image
        b.click('button.pf-v5-c-select__menu-item:contains("localhost:5000/my-busybox")')

        # Remote image, no pull latest image option
        b.wait_not_present("#run-image-dialog-pull-latest-image")

        # Create Container, image is pulled and should end up being "running"
        b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
        sel = " span:not(.downloading)"
        b.wait(lambda: self.getContainerAttr(container_name, "State", sel) in 'Running')
        self.execute(auth, f"docker exec {container_name} test ! -e /latest")

        # Now that we have downloaded an image, verify that selecting download latest image
        # downloads the latest image we now push to the registry. Note this image has a /latest file
        # to differnatiate it from the other local image.
        self.execute(True, f"docker push {new_image_sha} localhost:5000/my-busybox")
        self.execute(True, f"docker push {new_image_sha} localhost:6000/my-busybox")
        self.execute(True, f"docker rmi {new_image_sha}")

        container_name = "busybox-latest"

        b.click("#containers-containers button.pf-v5-c-button.pf-m-primary")
        b.set_input_text("#run-image-dialog-name", container_name)

        # Local registry
        b.set_input_text("#create-image-image-select-typeahead", "my-busybox")
        b.click('button.pf-v5-c-toggle-group__button:contains("Local")')

        # Select image
        b.click('button.pf-v5-c-select__menu-item:contains("localhost:5000/my-busybox")')

        # Pull the latest image
        b.set_checked("#run-image-dialog-pull-latest-image", True)

        # Create Container, image is pulled and should end up being "running"
        b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
        sel = " span:not(.downloading)"
        b.wait(lambda: self.getContainerAttr(container_name, "State", sel) in 'Running')
        # Verify that the latest file exists
        output = self.execute(auth, f"docker exec {container_name} ls -lh /latest").strip()
        self.assertNotIn("No such file or directory", output)

        # Test creating a container with
        if auth:
            container_name = "busybox-download-admin"
            b.click("#containers-containers button.pf-v5-c-button.pf-m-primary")

            # Start container as admin
            b.click('#run-image-dialog-owner-user')

            # Create Container, image is pulled and should end up being "Running"
            b.set_input_text("#run-image-dialog-name", container_name)

            b.set_input_text("#create-image-image-select-typeahead", IMG_BUSYBOX)
            b.click('button.pf-v5-c-toggle-group__button:contains("Local")')
            b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_BUSYBOX}")')

            b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
            b.wait(lambda: self.getContainerAttr(container_name, "State", sel) in 'Running')

    def testRunImageSystem(self):
        self._testRunImage(True)

    def testRunImageUser(self):
        self._testRunImage(False)

    def _testRunImage(self, auth):
        b = self.browser
        m = self.machine

        # Just drop user images so we can use simpler selectors
        if auth:
            self.execute(False, "docker rmi --all")

        self.login(auth)

        b.click("#containers-images button.pf-v5-c-expandable-section__toggle")

        b.wait_in_text("#containers-images", IMG_BUSYBOX)
        b.wait_in_text("#containers-images", IMG_ALPINE)
        if auth:
            b.wait_not_in_text("#containers-images", "admin")

        # Check command in alpine
        b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_ALPINE}")')
        b.click(f'#containers-images tbody tr:contains("{IMG_ALPINE}") .ct-container-create')
        b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')
        # depending on the precise container, this can be /bin/sh or /bin/ash
        cmd = self.execute(auth, 'docker image inspect --format "{{.Config.Cmd}}" ' + IMG_ALPINE)
        cmd = cmd.strip().replace('[', '').replace(']', '')
        b.wait_attr("#run-image-dialog-command", "value", cmd)
        b.click(".btn-cancel")

        # Open run image dialog
        b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")')
        b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create')
        b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')

        # Inspect and fill modal dialog
        b.wait_val("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST)

        # Check that there is autogenerated name and then overwrite it
        b.wait_not_val("#run-image-dialog-name", "")
        b.set_input_text("#run-image-dialog-name", "busybox-with-tty")

        b.wait_visible("#run-image-dialog-command[value='sh']")

        # Check memory configuration
        # Only works with CGroupsV2
        if auth or self.has_cgroupsV2:
            b.set_checked("#run-image-dialog-memory-limit-checkbox", True)
            b.wait_visible("#run-image-dialog-memory-limit-checkbox:checked")
            b.wait_visible('#run-image-dialog-memory-limit input[value="512"]')
            b.set_input_text("#run-image-dialog-memory-limit input[type=number]", "0.5")
            b.set_val('#memory-unit-select', "GB")

        # CPU shares work only with system containers
        if auth:
            # Check that the checkbox is enabled when clicked on the field
            b.wait_visible("#run-image-dialog-cpu-priority-checkbox:not(:checked)")
            b.click('#run-image-cpu-priority')
            b.wait_visible("#run-image-dialog-cpu-priority-checkbox:checked")
            b.set_checked("#run-image-dialog-cpu-priority-checkbox", False)

            b.set_checked("#run-image-dialog-cpu-priority-checkbox", True)
            b.wait_visible("#run-image-dialog-cpu-priority-checkbox:checked")
            b.wait_visible('#run-image-dialog-cpu-priority input[value="1024"]')
            b.set_input_text("#run-image-dialog-cpu-priority input[type=number]", "512")
        else:
            b.wait_not_present("#run-image-dialog-cpu-priority-checkbox")

        # Enable tty
        b.set_checked("#run-image-dialog-tty", True)

        # Set up command line
        b.set_input_text('#run-image-dialog-command',
                         "sh -c 'for i in $(seq 20); do sleep 1; echo $i; done; sleep infinity'")

        if auth:
            # Set restart policy to 3 retries
            b.set_val("#run-image-dialog-restart-policy", "on-failure")
            b.set_input_text('#run-image-dialog-restart-retries input', '3')
        else:  # no lingering enabled so it's disabled
            b.wait_not_present("#run-image-dialog-restart-policy")

        # Switch to Integration tab
        b.click("#pf-tab-1-create-image-dialog-tab-integration")

        # Configure published ports
        b.click('.publish-port-form .btn-add')
        b.set_input_text('#run-image-dialog-publish-0-host-port', '6000')
        b.set_input_text('#run-image-dialog-publish-0-container-port', '5000')
        b.click('.publish-port-form .btn-add')
        b.set_input_text('#run-image-dialog-publish-1-ip-address', '127.0.0.1')
        b.set_input_text('#run-image-dialog-publish-1-host-port', '6001')
        b.set_input_text('#run-image-dialog-publish-1-container-port', '5001')
        b.set_val('#run-image-dialog-publish-1-protocol', "udp")
        b.click('.publish-port-form .btn-add')
        b.set_input_text('#run-image-dialog-publish-2-ip-address', '7001')
        b.set_input_text('#run-image-dialog-publish-2-host-port', '7001')
        b.click('#run-image-dialog-publish-2-btn-close')
        b.click('.publish-port-form .btn-add')
        b.set_input_text('#run-image-dialog-publish-3-container-port', '8001')
        b.click('.publish-port-form .btn-add')
        b.set_input_text('#run-image-dialog-publish-4-ip-address', '127.0.0.2')
        b.set_input_text('#run-image-dialog-publish-4-container-port', '9001')

        # Configure env
        b.click('.env-form .btn-add')
        b.set_input_text('#run-image-dialog-env-0-key', 'APPLE')
        b.set_input_text('#run-image-dialog-env-0-value', 'ORANGE')
        b.click('.env-form .btn-add')
        b.set_input_text('#run-image-dialog-env-1-key', 'PEAR')
        b.set_input_text('#run-image-dialog-env-1-value', 'BANANA')
        b.click('.env-form .btn-add')
        b.set_input_text('#run-image-dialog-env-2-key', 'MELON')
        b.set_input_text('#run-image-dialog-env-2-value', 'GRAPE')
        b.click('#run-image-dialog-env-2-btn-close')
        b.click('.env-form .btn-add')
        # Test inputting an key=var entry
        b.set_val('#run-image-dialog-env-3-value',
                  "RHUBARB=STRAWBERRY DURIAN=LEMON TEST_URL=wss://cockpit/?start=1&stop=0")
        # set_val does not trigger onChange so append a space.
        b.set_input_text('#run-image-dialog-env-3-value', ' ', append=True, value_check=False)

        b.click('.env-form .btn-add')
        b.set_input_text('#run-image-dialog-env-6-key', 'HOSTNAME')
        b.set_input_text('#run-image-dialog-env-6-value', 'busybox')

        # Test inputting a var with = in it doesn't reset key
        b.click('.env-form .btn-add')
        b.set_input_text('#run-image-dialog-env-7-key', 'TEST')
        b.set_input_text('#run-image-dialog-env-7-value', 'REBASE=1')

        # Configure volumes
        b.click('.volume-form .btn-add')
        rodir, rwdir = m.execute("mktemp; mktemp").split('\n')[:2]
        m.execute(f"chown admin:admin {rodir}")
        m.execute(f"chown admin:admin {rwdir}")
        b.set_checked("#run-image-dialog-volume-0-mode", False)

        if self.has_selinux:
            b.set_val('#run-image-dialog-volume-0-selinux', "z")
        else:
            b.wait_not_present('#run-image-dialog-volume-0-selinux')

        b.set_file_autocomplete_val("#run-image-dialog-volume-0 .pf-v5-c-select", rodir)
        b.key_press(["\r"])
        b.set_input_text('#run-image-dialog-volume-0-container-path', '/tmp/ro')
        ro_label = m.execute(f"ls -dZ {rodir}").split(" ")[0]
        b.key_press(["\r"])
        b.click('.volume-form .btn-add')
        b.wait_visible('#run-image-dialog-volume-1')
        b.click('#run-image-dialog-volume-1-btn-close')
        b.wait_not_present('#run-image-dialog-volume-1')
        b.click('.volume-form .btn-add')

        if auth:
            b.assert_pixels(".pf-v5-c-modal-box", "integration",
                            ignore=["#run-image-dialog-volume-0 .pf-v5-c-select__toggle-typeahead"],
                            skip_layouts=["rtl"])

        if self.has_selinux:
            b.set_val('#run-image-dialog-volume-2-selinux', "Z")
        else:
            b.wait_not_present('#run-image-dialog-volume-2-selinux')

        b.set_file_autocomplete_val("#run-image-dialog-volume-2 .pf-v5-c-select", rwdir)
        b.key_press(["\r"])
        b.set_input_text('#run-image-dialog-volume-2-container-path', '/tmp/rw')
        rw_label = m.execute(f"ls -dZ {rwdir}").split(" ")[0]

        b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
        b.wait_not_present("div.pf-v5-c-modal-box")
        self.waitContainerRow(IMG_BUSYBOX)
        sha = self.execute(auth, "docker inspect --format '{{.Id}}' busybox-with-tty").strip()
        self.waitContainer(sha, auth, name='busybox-with-tty', image=IMG_BUSYBOX,
                           cmd='sh -c "for i in $(seq 20); do sleep 1; echo $i; done; sleep infinity"',
                           state='Running', owner="system" if auth else "admin")
        hasTTY = self.execute(auth, "docker inspect --format '{{.Config.Tty}}' busybox-with-tty").strip()
        self.assertEqual(hasTTY, 'true')
        # Only works with CGroupsV2
        if auth or self.has_cgroupsV2:
            memory = self.execute(auth, "docker inspect --format '{{.HostConfig.Memory}}' busybox-with-tty").strip()
            self.assertEqual(memory, '500000000')

        if auth:
            cpuShares = self.execute(auth,
                                     "docker inspect --format '{{.HostConfig.CpuShares}}' busybox-with-tty").strip()
            self.assertEqual(cpuShares, '512')

        restartPolicy = self.getRestartPolicy(auth, "busybox-with-tty")
        if auth:
            self.assertEqual(restartPolicy, '{on-failure 3}')
        else:
            # No restart policy
            # format changed in podman 5.1 (https://github.com/containers/podman/pull/22322)
            self.assertIn(restartPolicy, ['{ 0}', '{no 0}'])

        b.wait(lambda: "3" in self.execute(auth, "docker logs busybox-with-tty"))

        self.toggleExpandedContainer(IMG_BUSYBOX)

        b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Created") + dd', 'today at')

        b.click(".pf-m-expanded button:contains('Integration')")

        b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd',
                       '0.0.0.0:6000 \u2192 5000/tcp')
        b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd',
                       '127.0.0.1:6001 \u2192 5001/udp')
        b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd',
                       '127.0.0.2:')
        b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd',
                       ' \u2192 8001/tcp')
        b.wait_not_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd',
                           '7001/tcp')

        ports = self.execute(auth, "docker inspect --format '{{.NetworkSettings.Ports}}' busybox-with-tty")
        self.assertIn('5000/tcp:[{ 6000}]', ports)
        self.assertIn('5001/udp:[{127.0.0.1 6001}]', ports)
        self.assertIn('8001/tcp:[{', ports)
        self.assertIn('9001/tcp:[{127.0.0.2 ', ports)
        self.assertNotIn('7001/tcp', ports)

        env_select = f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Environment variables") + dd'
        b.wait_in_text(env_select, 'APPLE=ORANGE')
        b.wait_in_text(env_select, 'PEAR=BANANA')
        b.wait_in_text(env_select, 'RHUBARB=STRAWBERRY')
        b.wait_in_text(env_select, 'DURIAN=LEMON')
        b.wait_in_text(env_select, 'TEST_URL=wss://cockpit/?start=1&stop=0')
        b.wait_in_text(env_select, 'HOSTNAME=busybox')
        b.wait_in_text(env_select, 'TEST=REBASE=1')
        # variables are present in env but are not displayed in the UI
        b.wait_not_in_text(env_select, 'container=docker')
        b.wait_not_in_text(env_select, 'TERM=xterm')
        b.wait_not_in_text(env_select, 'HOME=/root')
        b.wait_not_in_text(env_select, 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin')

        b.click(".container-integration button:contains('Show more')")
        # previously hidden variables are now visible
        b.wait_in_text(env_select, 'container=docker')
        b.wait_in_text(env_select, 'TERM=xterm')
        b.wait_in_text(env_select, 'HOME=/root')
        b.wait_in_text(env_select, 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin')

        env = self.execute(auth, "docker exec busybox-with-tty env")
        self.assertIn('APPLE=ORANGE', env)
        self.assertIn('PEAR=BANANA', env)
        self.assertIn('RHUBARB=STRAWBERRY', env)
        self.assertIn('DURIAN=LEMON', env)
        self.assertIn('HOSTNAME=busybox', env)
        self.assertIn('TEST=REBASE=1', env)
        self.assertIn('container=docker', env)
        self.assertIn('TERM=xterm', env)
        self.assertIn('HOME=/root', env)
        self.assertIn('PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', env)
        self.assertNotIn('MELON=GRAPE', env)

        vol_select = f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Volumes") + dd'
        b.wait_in_text(vol_select, f"{rodir} \u2192 /tmp/ro")
        b.wait_in_text(vol_select, f"{rwdir} \u2194 /tmp/rw")

        romnt = self.execute(auth, "docker exec busybox-with-tty cat /proc/self/mountinfo | grep /tmp/ro")
        self.assertIn('ro', romnt)
        self.assertIn(rodir[4:], romnt)
        rwmnt = self.execute(auth, "docker exec busybox-with-tty cat /proc/self/mountinfo | grep /tmp/rw")
        self.assertIn('rw', rwmnt)
        self.assertIn(rwdir[4:], rwmnt)

        if self.has_selinux:
            # rw was set to :Z so it should change, but not be shared
            rw_label_new = m.execute(f"ls -dZ {rwdir}").split(" ")[0]
            self.assertNotEqual(rw_label, rw_label_new)
            self.assertRegex(rw_label_new, r"container_file_t:s0:c\d*,c\d*$")

            # ro was set to :z to it should change and be shared
            ro_label_new = m.execute(f"ls -dZ {rodir}").split(" ")[0]
            self.assertNotEqual(ro_label, ro_label_new)
            self.assertRegex(ro_label_new, "container_file_t:s0$")

        def get_int(n):
            try:
                return int(n)
            except ValueError:
                return 0

        b.wait_not_present("button:contains('Health check logs')")
        b.click(".pf-m-expanded button:contains('Logs')")
        b.wait_text(".pf-m-expanded .container-logs .xterm-accessibility-tree > div:nth-child(1)", "1")

        # firefox optimizes these out when not visible
        b.eval_js("""
            document.querySelector('.pf-m-expanded .container-logs .xterm-accessibility-tree').scrollIntoView()
        """)
        b.wait_in_text(".pf-m-expanded .container-logs .xterm-accessibility-tree", "6")

        b.click(".pf-m-expanded button:contains('Console')")
        b.wait(lambda:
               get_int(b.text(".pf-m-expanded .container-terminal .xterm-accessibility-tree > div:nth-child(3)")) > 7)

        # Create another instance without port publishing
        b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")')
        self.toggleExpandedContainer(IMG_BUSYBOX)
        b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create')
        b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')

        b.wait_val("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST)
        b.set_input_text("#run-image-dialog-name", "busybox-without-publish")

        # Set up command line
        b.set_input_text('#run-image-dialog-command',
                         "sh -c 'for i in $(seq 20); do echo $i; sleep 3; done; sleep infinity'")

        # Run without tty, console should be able to `exec`
        b.set_checked("#run-image-dialog-tty", False)

        b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
        b.wait_not_present("div.pf-v5-c-modal-box")

        self.waitContainerRow("busybox-without-publish")
        self.toggleExpandedContainer("busybox-without-publish")
        b.wait_not_present("""
            #containers-containers tbody tr:contains("busybox-without-publish") + tr dt:contains("Ports")
        """)

        # Rootless only works with CGroupsV2
        if auth or self.has_cgroupsV2:
            cpuShares = self.execute(auth, """
                docker inspect --format '{{.HostConfig.CpuShares}}' busybox-without-publish
            """).strip()
            # docker ≥ 1.8 translates 0 default into actual value
            self.assertIn(cpuShares, ['0', '1024'])

        b.set_val("#containers-containers-filter", "all")

        b.click(".pf-m-expanded button:contains('Console')")
        b.wait_in_text(".pf-m-expanded .xterm-accessibility-tree", "/ # ")
        b.focus(".pf-m-expanded .xterm-helper-textarea")
        b.key_press('clear\r')
        b.wait_not_in_text(".pf-m-expanded .xterm-accessibility-tree", "clear")
        b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(1)", "/ # ")
        b.key_press('echo hello\r')
        b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(2)", "hello")
        b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(3)", "/ # ")
        b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(1)", "/ # echo hello")

        b.go("#/?name=tty")
        self.check_containers(["busybox-with-tty"], ["busybox-without-publish"])
        b.go("#/?name=busy")
        self.check_containers(["busybox-with-tty", "busybox-without-publish"], [])

        b.set_input_text('#containers-filter', 'tty')
        self.check_containers(["busybox-with-tty"], ["busybox-without-publish"])
        self.check_images([], [IMG_ALPINE, IMG_BUSYBOX, IMG_REGISTRY])
        b.set_input_text('#containers-filter', 'busy')
        b.wait_js_cond('window.location.hash === "#/?name=busy"')
        self.check_containers(["busybox-with-tty", "busybox-without-publish"], [])
        self.check_images([IMG_BUSYBOX], [IMG_ALPINE, IMG_REGISTRY])
        b.set_input_text('#containers-filter', 'alpine')
        b.wait_js_cond('window.location.hash === "#/?name=alpine"')
        self.check_containers([], ["busybox-with-tty", "busybox-without-publish"])
        self.check_images([IMG_ALPINE], [IMG_BUSYBOX, IMG_REGISTRY])
        b.set_input_text('#containers-filter', '')
        self.check_containers(["busybox-with-tty", "busybox-without-publish"], [])
        self.check_images([IMG_ALPINE, IMG_BUSYBOX, IMG_REGISTRY], [])
        b.wait_js_cond('window.location.hash === "#/"')

        self.filter_containers("running")
        id_with_tty = self.execute(auth, "docker inspect --format '{{.Id}}' busybox-with-tty").strip()

        container_sel = f'#containers-images tbody tr:contains("{IMG_BUSYBOX}")'
        b.click(f'{container_sel} td.pf-v5-c-table__toggle button')
        # running container, just selects it, but leaves "Only running" alone
        b.click(f"{container_sel} + tr div.ct-listing-panel-body dt:contains('Used by') + dd button:contains('busybox-with-tty')")  # noqa: E501
        b.wait_js_cond('window.location.hash === "#' + id_with_tty + '"')
        b.wait_val("#containers-containers-filter", "running")
        # FIXME: expanding running container details does not actually work right now
        # b.wait_in_text("#containers-containers tr.pf-m-expanded .container-details", "sleep infinity")
        # stopped container, switches to showing all containers

        # Create a container without starting it
        self.filter_containers("all")
        container_name = "busybox-not-started"
        b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")')
        b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create')
        b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')

        b.wait_val("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST)
        b.set_input_text("#run-image-dialog-name", container_name)
        b.set_input_text("#run-image-dialog-command", "sh -c sleep infinity")

        b.click('.pf-v5-c-modal-box__footer #create-image-create-btn')
        b.wait_not_present("div.pf-v5-c-modal-box")

        sha = self.execute(auth, "docker inspect --format '{{.Id}}' " + container_name).strip()
        self.waitContainer(sha, auth, name=container_name, image=IMG_BUSYBOX, state=['Configured', 'Created'])

        self.filter_containers("running")
        b.wait_not_in_text("#containers-containers", "busybox-not-started")
        container_sel = f"#containers-images tbody tr:contains('{IMG_BUSYBOX}') + tr div.ct-listing-panel-body"
        b.click(f"{container_sel} dt:contains('Used by') + dd button:contains('busybox-not-started')")
        b.wait_js_cond(f"window.location.hash === '#{sha}'")
        b.wait_val("#containers-containers-filter", "all")
        b.wait_in_text("#containers-containers", "busybox-not-started")
        # auto-expands container details
        b.wait_in_text("#containers-containers tbody tr:contains('busybox-not-started') + tr", "sleep infinity")

        b.click(f'#containers-images tbody tr:contains("{IMG_ALPINE}") td.pf-v5-c-table__toggle button')
        b.wait_in_text(f"#containers-images tbody tr:contains('{IMG_ALPINE}') td[data-label='Used by']", 'unused')

        b.set_input_text('#containers-filter', 'foobar')
        b.wait_in_text('#containers-containers .pf-v5-c-empty-state', 'No containers that match the current filter')
        b.wait_in_text('#containers-images .pf-v5-c-empty-state', 'No images that match the current filter')
        b.set_input_text('#containers-filter', '')

        if not auth or not self.machine.ostree_image:  # don't kill ws container
            # Ubuntu 22.04 has old docker that does not know about --time
            if m.image != 'ubuntu-2204':
                # Remove all containers first as it is not possible to set --time 0 to rmi command
                self.execute(auth, "docker rm --all --force --time 0")
            self.execute(auth, "docker rmi -af")
            b.wait_in_text('#containers-containers .pf-v5-c-empty-state', 'No containers')
            b.set_val("#containers-containers-filter", "running")
            b.wait_in_text('#containers-containers .pf-v5-c-empty-state', 'No running containers')
            b.wait_in_text('#containers-images .pf-v5-c-empty-state', 'No images')

    def check_content(self, kind, present, not_present):
        b = self.browser
        for item in present:
            b.wait_visible(f'#containers-{kind} tbody tr:first-child:contains({item})')
        for item in not_present:
            b.wait_not_present(f'#containers-{kind} tbody tr:first-child:contains({item})')

    def check_containers(self, present, not_present):
        self.check_content("containers", present, not_present)

    def check_images(self, present, not_present):
        self.check_content("images", present, not_present)

    def waitContainer(self, row_id, auth, name="", image="", cmd="", owner="", state=None, pod="no-pod"):
        """Check the container with row_name has the expected values
            "image" can be substring, "state" might be string or array of possible states, other are
            checked for exact match.
        """
        sel = "#containers-containers #table-" + pod + f" tbody tr[data-row-id=\"{row_id}{auth}\"]".lower()
        b = self.browser
        if name:
            b.wait_text(sel + " .container-name", name)
        if image:
            b.wait_in_text(sel + " .container-block small:nth-child(2)", image)
        if cmd:
            b.wait_text(sel + " .container-block small:last-child", cmd)
        if owner:
            if owner == "system":
                b.wait_text(sel + " td[data-label=Owner]", owner)
            else:
                b.wait_text(sel + " td[data-label=Owner]", "user: " + owner)
        if state is not None:
            if not isinstance(state, list):
                state = [state]
            b.wait(lambda: b.text(sel + " td[data-label=State]") in state)

    def filter_containers(self, value):
        """Use dropdown menu in the header to filter containers"""
        b = self.browser
        b.set_val("#containers-containers-filter", value)

    def confirm_modal(self, text):
        """Wait for the pop up window and click the button with text"""
        b = self.browser
        b.click(f".pf-v5-c-modal-box footer button:contains({text})")
        b.wait_not_present(f".pf-v5-c-modal-box footer button:contains({text})")

    def testPruneUnusedImagesSystem(self):
        self._testPruneUnusedImagesSystem(True)

    def testPruneUnusedImagesUser(self):
        self._testPruneUnusedImagesSystem(False)

    @testlib.skipOstree("no root login available on ostree")
    def testPruneUnusedImagesRoot(self):
        self._testPruneUnusedImagesSystem(False, True)

    def _testPruneUnusedImagesSystem(self, auth, root=False):
        b = self.browser
        if root:
            self.login_and_go("/docker", user="root", enable_root_login=True)
            b.wait_visible("#app")
        else:
            self.login(auth)

        leftover_images = 1
        # cockpit-ws image
        if self.machine.ostree_image and auth:
            leftover_images += 1

        # By default we have 3 unused images, start one.
        self.execute(auth or root, f"docker run -d --name used_image --stop-timeout 0 {IMG_ALPINE} sh")
        b.click("#image-actions-dropdown")
        b.click("#prune-unused-images-button")

        if auth:
            b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li",
                           (self.user_images_count + self.system_images_count) - leftover_images)
        elif root:
            b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li",
                           self.system_images_count - leftover_images)
        else:
            b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li",
                           self.user_images_count - leftover_images)
        b.click(".pf-v5-c-modal-box button:contains(Prune)")

        # When being superuser, admin images are also removed
        if auth:
            self.waitNumImages(leftover_images)
            checkImage(b, IMG_ALPINE, "system")
        else:
            self.waitNumImages(leftover_images)
            # Two images removed, one in use kept
            b.wait_not_present(f"#containers-images:contains('{IMG_BUSYBOX}')")
            b.wait_not_present(f"#containers-images:contains('{IMG_REGISTRY}')")
            b.wait_visible(f"#containers-images:contains('{IMG_ALPINE}')")

        # Prune button should now be disabled
        b.click("#image-actions-dropdown")
        b.wait_visible(".pf-m-disabled.pf-v5-c-menu__list-item:contains(Prune unused images)")

    def testPruneUnusedImagesSystemSelections(self):
        """ Test the prune unused images selection options"""
        b = self.browser
        self.login(True)

        b.click("#image-actions-dropdown")
        b.click("button:contains(Prune unused images)")

        # Deselect both
        b.click("#deleteSystemImages")
        b.click("#deleteUserImages")
        b.wait_visible(".pf-v5-c-modal-box button:contains(Prune):disabled")

        # Admin / user images are selected
        expected_images = self.user_images_count + self.system_images_count
        if self.machine.ostree_image:
            expected_images -= 1
        b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li", expected_images)
        # Select user images
        b.click("#deleteUserImages")
        b.click(".pf-v5-c-modal-box button:contains(Prune)")

        # System images are left over
        self.waitNumImages(self.system_images_count)
        checkImage(b, IMG_ALPINE, "system")
        checkImage(b, IMG_BUSYBOX, "system")
        checkImage(b, IMG_REGISTRY, "system")

        # Pruning again, should delete all system images
        b.click("#image-actions-dropdown")
        b.click("button:contains(Prune unused images)")
        b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li",
                       self.system_images_count - 1 if self.machine.ostree_image else self.system_images_count)
        b.click(".pf-v5-c-modal-box button:contains(Prune)")
        self.waitNumImages(1 if self.machine.ostree_image else 0)

        # Prune button should now be disabled
        b.click("#image-actions-dropdown")
        b.wait_visible(".pf-v5-c-menu__list-item.pf-m-disabled:contains(Prune unused images)")

    def testPruneUnusedContainersSystem(self):
        self._testPruneUnusedContainersSystem(True)

    def testPruneUnusedContainersUser(self):
        self._testPruneUnusedContainersSystem(False)

    def _testPruneUnusedContainersSystem(self, auth):
        """Test the prune unused container image dialog"""

        b = self.browser
        self.login(auth)

        # Create running and non-running containers
        self.execute(auth, "docker pod create --name pod")
        notrunninginpodId = self.execute(auth, f"""
            docker run --name inpod --pod pod -tid {IMG_BUSYBOX} sh -c 'exit 1'""").strip()
        runninginpodId = self.execute(auth, f"""
            docker run --name inpodrunning --pod pod -tid {IMG_BUSYBOX} sh -c 'sleep infinity'""").strip()

        self.execute(auth, f"docker run --name notrunning -tid {IMG_BUSYBOX} sh -c  'exit 1'")
        self.execute(auth, f"docker run --name containerrunning -tid {IMG_BUSYBOX} sh -c 'sleep infinity'")

        # Create containers for the opposite of what we are, admin or super admin
        if auth:
            self.execute(False, f"docker run --name adminnotrunning -tid {IMG_BUSYBOX} sh 'exit 1'")
            b.wait(lambda: self.getContainerAttr("adminnotrunning", "State") in NOT_RUNNING)
            self.execute(False, f"docker run --name adminrunning -tid {IMG_BUSYBOX} sh -c 'sleep infinity'")
            b.wait(lambda: self.getContainerAttr("adminrunning", "State") == "Running")

        b.click("#containers-actions-dropdown")
        b.click("button:contains(Prune unused containers)")

        if auth:
            b.wait_in_text(".pf-v5-c-modal-box__body tbody:nth-of-type(1) td[data-label=Name]", "adminnotrunning")
            b.wait_in_text(".pf-v5-c-modal-box__body tbody:nth-of-type(2) td[data-label=Name]", "notrunning")
        else:
            b.wait_in_text(".pf-v5-c-modal-box__body tbody td[data-label=Name]", "notrunning")

        b.click(".pf-v5-c-modal-box button:contains(Prune)")
        b.wait_not_present(".pf-v5-c-modal-box__body")

        if auth:
            self.waitContainerRow("notrunning", False)
            self.waitContainerRow("adminnotrunning", False)
        else:
            self.waitContainerRow("notrunning", False)

        # Verify running containers still exists
        self.waitContainerRow("containerrunning")
        pods = [{"name": "inpod", "state": "Exited", "id": notrunninginpodId,
                 "image": IMG_BUSYBOX, "command": 'sh -c "exit 1"'},
                {"name": "inpodrunning", "state": "Running", "id": runninginpodId,
                 "image": IMG_BUSYBOX, "command": 'sh -c "sleep infinity"'}]
        self.waitPodContainer("pod", pods, auth)

    def testCreateContainerValidation(self):
        def validateField(groupSelector, value, errorMessage, resetValue=""):
            b.set_input_text(f"{groupSelector} input", value)
            b.wait_visible(".pf-v5-c-modal-box__footer #create-image-create-run-btn:not(:disabled)")
            b.wait_in_text(f"{groupSelector} .pf-v5-c-helper-text__item-text", errorMessage)
            b.wait_visible(".pf-v5-c-modal-box__footer #create-image-create-run-btn[aria-disabled=true]")
            # Reset to acceptable value and verify the validation message is not present
            b.set_input_text(f"{groupSelector} input", resetValue)
            b.wait_not_present(f"{groupSelector} .pf-v5-c-helper-text__item-text")
            b.wait_visible(".pf-v5-c-modal-box__footer #create-image-create-run-btn:not(:disabled)")

        # Test the validation errors

        # complaint about port conflict
        self.allow_browser_errors("error: Container failed to be started:.*")
        self.allow_browser_errors("No routable interface.*")
        self.allow_browser_errors(".*ddress already in use.*5000.*")
        b = self.browser
        self.login(False)
        container_name = 'portused'

        # Start a docker container which uses a port
        self.execute(False, f"docker run -d -p 5000:5000 --name registry --stop-timeout 0 {IMG_REGISTRY}")
        b.click("#containers-images button.pf-v5-c-expandable-section__toggle")

        b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")')
        b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create')
        b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')

        validateField("#image-name-group", "registry", "Name already in use")

        # Switch to Integration tab
        b.click("#pf-tab-1-create-image-dialog-tab-integration")

        # Test validation of port mapping
        b.click('.publish-port-form .btn-add')
        b.set_input_text("#run-image-dialog-publish-0-container-port-group input", "1")
        validateField("#run-image-dialog-publish-0-ip-address-group", "abcd", "valid IP address")
        validateField("#run-image-dialog-publish-0-host-port-group", "-1", "1 to 65535")
        validateField("#run-image-dialog-publish-0-host-port-group", "99999", "1 to 65535")
        validateField("#run-image-dialog-publish-0-container-port-group", "-1", "1 to 65535", resetValue="1")
        validateField("#run-image-dialog-publish-0-container-port-group", "", "must not be empty", resetValue="1")
        validateField("#run-image-dialog-publish-0-container-port-group", "99999", "1 to 65535", resetValue="1")

        # Test validation of volumes
        b.click('.volume-form .btn-add')
        b.set_input_text("#run-image-dialog-volume-0-container-path-group input", "/somepath")
        validateField("#run-image-dialog-volume-0-container-path-group", "", "not be empty", resetValue="/somepath")

        # Test validation of environment variables
        b.click('.env-form .btn-add')
        b.set_input_text("#run-image-dialog-env-0-key-group input", "sometext")
        validateField("#run-image-dialog-env-0-key-group", "", "must not be empty", resetValue="sometext")

        b.set_input_text("#run-image-dialog-name", container_name)

        # Port address is already in use
        b.set_input_text('#run-image-dialog-publish-0-host-port', '5000')
        b.set_input_text('#run-image-dialog-publish-0-container-port', '5000')
        b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
        # Can be "[aA]ddress"
        b.wait_in_text(".pf-v5-c-alert", "ddress already in use")

        # Changing the port should allow creation of container
        b.set_input_text('#run-image-dialog-publish-0-host-port', '5001')
        b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
        b.wait_not_present("#run-image-dialog-name")

        self.waitContainerRow(container_name)

        # Test validation JavaScript errors when removing invalid environment entries
        container_name = 'env-var-validation'
        b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create')
        b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')

        b.set_input_text("#run-image-dialog-name", container_name)
        b.click("#pf-tab-1-create-image-dialog-tab-integration")

        # Make sure our form validation does not crash when adding and removing invalid entries
        b.click('.env-form .btn-add')
        b.click('.env-form .btn-add')
        b.click('.env-form .btn-add')
        b.set_input_text("#run-image-dialog-env-1-key-group input", "something")
        b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')

        b.wait_in_text("#run-image-dialog-env-0-key-group .pf-v5-c-helper-text__item-text", "must not be empty")
        b.wait_in_text("#run-image-dialog-env-2-key-group .pf-v5-c-helper-text__item-text", "must not be empty")

        # remove invalid entries
        b.click('#run-image-dialog-env-0-btn-close')
        b.click('#run-image-dialog-env-2-btn-close')

        b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
        self.waitContainerRow(container_name)

    def _testHealthcheck(self, auth):
        b = self.browser

        # Just drop user images so we can use simpler selectors
        if auth:
            self.execute(False, f"docker rmi {IMG_BUSYBOX}")

        self.login(auth)

        b.click("#containers-images button.pf-v5-c-expandable-section__toggle")

        b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")')
        b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create')
        b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')

        b.set_input_text("#run-image-dialog-name", "healthy")

        b.click("#pf-tab-2-create-image-dialog-tab-healthcheck")
        b.set_input_text('#run-image-dialog-healthcheck-command', 'true')
        b.set_input_text('#run-image-healthcheck-interval input', '325')
        b.set_input_text('#run-image-healthcheck-timeout input', '35')
        b.set_input_text('#run-image-healthcheck-start-period input', '5')
        b.click('#run-image-healthcheck-retries .pf-v5-c-input-group__item:nth-child(1) button')
        b.wait_val("#run-image-healthcheck-retries input", 2)
        if auth:
            b.assert_pixels('.pf-v5-c-modal-box', "healthcheck-modal", skip_layouts=["rtl"])
        # Test that the healthcheck option is not available before docker 4.3
        if docker_version(self) < (4, 3, 0):
            b.wait_not_present("#run-image-healthcheck-action")
        b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')

        self.waitContainerRow("healthy")
        b.click("#containers-images button.pf-v5-c-expandable-section__toggle")

        healthy_sha = self.execute(auth, "docker inspect --format '{{.Id}}' healthy").strip()
        self.waitContainer(healthy_sha, auth, state='RunningHealthy')

        self.toggleExpandedContainer("healthy")
        b.click(".pf-m-expanded button:contains('Health check')")

        b.wait_in_text('#container-details-healthcheck dt:contains("Command") + dd', 'true')
        b.wait_in_text('#container-details-healthcheck dt:contains("Interval") + dd', '325 seconds')
        b.wait_in_text('#container-details-healthcheck dt:contains("Retries") + dd', '2')
        b.wait_in_text('#container-details-healthcheck dt:contains("Timeout") + dd', '35 seconds')
        b.wait_in_text('#container-details-healthcheck dt:contains("Start period") + dd', '5 seconds')
        b.wait_not_present('#container-details-healthcheck dt:contains("Failing streak")')
        if docker_version(self) >= (4, 3, 0):
            b.wait_in_text('#container-details-healthcheck dt:contains("When unhealthy") + dd', 'No action')

        self.assertEqual(self.execute(auth, "docker inspect --format '{{.Config.Healthcheck}}' healthy").strip(),
                         "{[true] 5s 5m25s 35s 2}")

        # single successful health check
        b.wait_in_text(".ct-listing-panel-body tbody tr", "Passed health run")
        b.wait_visible(".ct-listing-panel-body tbody:nth-of-type(1) svg.green")
        b.wait_not_present(".ct-listing-panel-body tbody:nth-of-type(2)")

        # Trigger run manually, adds one more healthy run
        self.performContainerAction("healthy", "Run health check")
        b.wait_visible(".ct-listing-panel-body tbody:nth-of-type(2) svg.green")
        b.wait_not_present(".ct-listing-panel-body tbody:nth-of-type(3)")

        self.toggleExpandedContainer("healthy")

        self.execute(auth, f"docker run --name sick -dt --health-cmd false --health-interval 5s {IMG_BUSYBOX}")
        self.waitContainerRow("sick")
        unhealthy_sha = self.execute(auth, "docker inspect --format '{{.Id}}' sick").strip()
        self.waitContainer(unhealthy_sha, auth, state='RunningUnhealthy')
        # Unhealthy should be first
        expected_ws = ""
        if auth and self.machine.ostree_image:
            expected_ws = "ws"
        b.wait_collected_text("#containers-containers .container-name", "healthysick" + expected_ws)

        self.toggleExpandedContainer("sick")
        b.click(".pf-m-expanded button:contains('Health check')")
        b.wait_visible(".pf-m-expanded .ct-listing-panel-body tbody:nth-of-type(1)")
        b.wait_visible(".pf-m-expanded .ct-listing-panel-body tbody:nth-of-type(4)")
        b.wait_visible(".pf-m-expanded .ct-listing-panel-body tbody:nth-of-type(2) svg.red")
        b.wait_visible('.pf-m-expanded #container-details-healthcheck dt:contains("Failing streak")')
        failures = int(b.text('.pf-m-expanded #container-details-healthcheck dt:contains("Failing streak") + dd'))
        self.assertGreater(failures, 3)
        if auth:
            b.wait_js_func("ph_count_check", ".pf-m-expanded table[aria-label=Logs] tbody tr", 5)
            b.assert_pixels(".pf-m-expanded .pf-v5-c-table__expandable-row-content",
                            "healthcheck-details",
                            ignore=["thead", "#container-details-healthcheck dt:contains('Failing streak') + dd",
                                    "td[data-label='Started at']"],
                            skip_layouts=["rtl"])

        self.toggleExpandedContainer("sick")
        b.click("#containers-images button.pf-v5-c-expandable-section__toggle")

        b.wait_visible('#containers-images td[data-label="Image"]:contains("busybox:latest")')
        b.click('#containers-images tbody tr:contains("busybox:latest") .ct-container-create')
        b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')

        # Test the health check action, only supported in docker 4.3 and later.
        # To test this we make a healthcheck which depends on a file, so when starting the
        # container is healthy, after we remove the file the healthcheck should fail and our
        # configured action should executed.
        if docker_version(self) < (4, 3, 0):
            return

        containername = "healthaction"
        b.set_input_text("#run-image-dialog-name", containername)
        b.set_input_text("#run-image-dialog-command", "/bin/sh -c 'echo 1 > /healthy && sleep infinity'")

        b.click("#pf-tab-2-create-image-dialog-tab-healthcheck")
        b.set_input_text('#run-image-dialog-healthcheck-command', '/bin/test -f /healthy')
        b.set_input_text('#run-image-healthcheck-interval input', '1')
        b.set_input_text('#run-image-healthcheck-timeout input', '1')
        b.click('#run-image-healthcheck-action-2')
        b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')

        self.waitContainerRow(containername)
        self.toggleExpandedContainer(containername)
        b.wait(lambda: self.getContainerAttr(containername, "State") == "RunningHealthy")
        b.click(".pf-m-expanded button:contains('Health check')")
        b.wait_in_text('.pf-m-expanded #container-details-healthcheck dt:contains("When unhealthy") + dd',
                       'Force stop')
        # Removing the file should kill the container
        status = self.execute(auth, f"docker exec {containername} rm -f /healthy").strip()
        b.wait(lambda: self.getContainerAttr(containername,
                                             "State", "span:not(.ct-badge-container-unhealthy)") in NOT_RUNNING)
        status = self.execute(auth, f"docker inspect --format '{{{{.State.Health.Status}}}}' {containername}").strip()
        self.assertEqual(status, "unhealthy")

    def testHealthcheckSystem(self):
        self._testHealthcheck(True)

    def testHealthcheckUser(self):
        self._testHealthcheck(False)


    def testdockerRestartEnabledSystem(self):
        self._testdockerRestartEnabled(True)

    def _testdockerRestartEnabled(self, auth):
        b = self.browser
        if auth:
            self.addCleanup(self.machine.execute, "systemctl disable docker-restart.service")

        # Drop user images for easy selection
        if auth:
            self.execute(False, f"docker rmi {IMG_BUSYBOX}")

        self.login(auth)
        b.click("#containers-images button.pf-v5-c-expandable-section__toggle")

        def create_container(name, policy=None):
            b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")')
            b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create')
            b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')

            b.set_input_text("#run-image-dialog-name", name)
            if policy:
                b.set_val("#run-image-dialog-restart-policy", "always")
            b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
            self.waitContainerRow(name)

        container_name = 'none'
        create_container(container_name)
        # format changed in podman 5.1 (https://github.com/containers/podman/pull/22322)
        self.assertIn(self.getRestartPolicy(auth, container_name), ['{ 0}', '{no 0}'])

        container_name = 'restart'
        create_container(container_name, 'always')
        self.assertEqual(self.getRestartPolicy(auth, container_name), '{always 0}')
        if auth:
            dockerRestartEnabled = self.execute(True, "systemctl is-enabled docker-restart.service || true").strip()
        self.assertEqual(dockerRestartEnabled, 'enabled')

    def testPauseResumeContainerSystem(self):
        self._testPauseResumeContainer(True)

    def testPauseResumeContainerUser(self):
        # rootless cgroupv1 containers do not support pausing
        if not self.has_cgroupsV2:
            return
        self._testPauseResumeContainer(False)

    def _testPauseResumeContainer(self, auth):
        b = self.browser
        container_name = "pauseresume"

        self.execute(auth, f"docker run -dt --name {container_name} --stop-timeout 0 {IMG_ALPINE}")
        self.login(auth)

        self.waitContainerRow(container_name)
        self.toggleExpandedContainer(container_name)
        b.wait_not_present(self.getContainerAction(container_name, 'Resume'))
        self.performContainerAction(container_name, "Pause")

        # show all containers and check status
        self.filter_containers('all')

        # Check that container details are not lost when the container is paused
        b.click(".pf-m-expanded button:contains('Integration')")
        b.wait_visible(f'#containers-containers tr:contains("{IMG_ALPINE}") dt:contains("Environment variables")')

        b.wait(lambda: self.getContainerAttr(container_name, "State") == "Paused")
        b.wait_not_present(self.getContainerAction(container_name, 'Pause'))
        self.performContainerAction(container_name, "Resume")
        b.wait(lambda: self.getContainerAttr(container_name, "State") == "Running")

    def testRenameContainerSystem(self):
        self._testRenameContainer(True)

    def _testRenameContainer(self, auth):
        b = self.browser
        container_name = "rename"
        container_name_new = "rename-new"

        self.execute(auth, f"docker container create -t --name {container_name} {IMG_BUSYBOX}")
        self.login(auth)

        self.filter_containers('all')

        self.waitContainerRow(container_name)
        self.toggleExpandedContainer(container_name)
        self.performContainerAction(container_name, "Rename")

        # the container name should be in the "Rename container" header
        b.wait_in_text("#pf-modal-part-1", container_name)
        b.set_input_text("#rename-dialog-container-name", "")
        b.wait_in_text("#commit-dialog-image-name-helper", "Container name is required")
        b.set_input_text("#rename-dialog-container-name", "banana???")
        b.wait_in_text("#commit-dialog-image-name-helper", "Name can only contain letters, numbers")

        b.set_input_text("#rename-dialog-container-name", container_name_new)
        b.click('#btn-rename-dialog-container')
        b.wait_not_present("#rename-dialog-container-name")

        self.execute(auth, f"docker inspect --format '{{{{.Id}}}}' {container_name_new}").strip()
        self.waitContainerRow(container_name_new)

        # rename using the enter key
        self.toggleExpandedContainer(container_name_new)
        self.performContainerAction(container_name_new, "Rename")

        container_name_new = "rename-new-enter"
        b.set_input_text("#rename-dialog-container-name", "")
        b.focus("#rename-dialog-container-name")
        b.key_press("\r")  # Simulate enter key
        b.wait_in_text("#commit-dialog-image-name-helper", "Container name is required")
        b.set_input_text("#rename-dialog-container-name", container_name_new)
        b.focus("#rename-dialog-container-name")
        b.key_press("\r")  # Simulate enter key
        b.wait_not_present("#rename-dialog-container-name")

        self.execute(auth, f"docker inspect --format '{{{{.Id}}}}' {container_name_new}").strip()
        self.waitContainerRow(container_name_new)

    def testMultipleContainers(self):
        self.login()

        # Create 31 containers
        for i in range(31):
            self.execute(True, f"docker run -dt --name container{i} --stop-timeout 0 {IMG_BUSYBOX}")

        self.waitContainerRow("container30")

        # Generic cleanup takes too long and timeouts, so remove these container manually one by one
        for i in range(31):
            self.execute(True, f"docker rm -f container{i}")

    def testSpecialContainers(self):
        m = self.machine
        b = self.browser

        toolbox_label = "com.github.containers.toolbox=true"
        distrobox_label = "manager=distrobox"

        container_1_id = m.execute(f"docker run -d --name container_1 -l {toolbox_label} {IMG_BUSYBOX}").strip()
        container_2_id = m.execute(f"docker run -d --name container_2 -l {distrobox_label} {IMG_BUSYBOX}").strip()

        self.login()

        self.waitContainerRow('container_1')
        self.waitContainerRow('container_2')

        container_1_sel = f"#containers-containers tbody tr[data-row-id=\"{container_1_id}{'true'}\"]"
        container_2_sel = f"#containers-containers tbody tr[data-row-id=\"{container_2_id}{'true'}\"]"

        b.wait_visible(container_1_sel + " .ct-badge-toolbox:contains('toolbox')")
        b.wait_visible(container_2_sel + " .ct-badge-distrobox:contains('distrobox')")

    # def testCreatePodSystem(self):
    #     self._createPod(True)

    # def testCreatePodUser(self):
    #     self._createPod(False)

    # def _createPod(self, auth):
    #     b = self.browser
    #     m = self.machine
    #     pod_name = "testpod1"

    #     self.login(auth)

        # b.click("#containers-containers-create-pod-btn")
        # b.set_input_text("#create-pod-dialog-name", "")
        # b.wait_visible(".pf-v5-c-modal-box__footer #create-pod-create-btn:disabled")
        # b.wait_in_text("#pod-name-group .pf-v5-c-helper-text__item-text", "Invalid characters")

        # b.set_input_text("#create-pod-dialog-name", pod_name)
        # b.wait_visible(".pf-v5-c-modal-box__footer #create-pod-create-btn:not(:disabled)")

        # b.click('.publish-port-form .btn-add')
        # b.set_input_text("#create-pod-dialog-publish-0-container-port-group input", "-1")
        # b.click(".pf-v5-c-modal-box__footer #create-pod-create-btn")
        # b.wait_in_text("#create-pod-dialog-publish-0-container-port-group .pf-v5-c-helper-text__item-text",
        #                "1 to 65535")
        # b.click("#create-pod-dialog-publish-0-btn-close")

    #     if auth:
    #         b.wait_visible("#create-pod-dialog-owner-system:checked")
    #     else:
    #         b.wait_not_present("#create-pod-dialog-owner-system")

        # Ports
        # b.click('.publish-port-form .btn-add')
        # b.set_input_text('#create-pod-dialog-publish-1-host-port', '6000')
        # b.set_input_text('#create-pod-dialog-publish-1-container-port', '5000')
        # b.click('.publish-port-form .btn-add')
        # b.set_input_text('#create-pod-dialog-publish-2-ip-address', '127.0.0.1')
        # b.set_input_text('#create-pod-dialog-publish-2-host-port', '6001')
        # b.set_input_text('#create-pod-dialog-publish-2-container-port', '5001')
        # b.set_val('#create-pod-dialog-publish-2-protocol', "udp")
        # b.click('.publish-port-form .btn-add')
        # b.set_input_text('#create-pod-dialog-publish-3-ip-address', '127.0.0.2')
        # b.set_input_text('#create-pod-dialog-publish-3-container-port', '9001')

        # Volumes
        # if self.machine.image not in ["ubuntu-2204"]:
        #     b.click('.volume-form .btn-add')
        #     rodir, rwdir = m.execute("mktemp; mktemp").split('\n')[:2]
        #     m.execute(f"chown admin:admin {rodir}")
        #     m.execute(f"chown admin:admin {rwdir}")

    #         if self.has_selinux:
    #             b.set_val('#create-pod-dialog-volume-0-selinux', "z")
    #         else:
    #             b.wait_not_present('#create-pod-dialog-volume-0-selinux')

            # b.set_file_autocomplete_val("#create-pod-dialog-volume-0 .pf-v5-c-select", rodir)
            # b.set_input_text('#create-pod-dialog-volume-0-container-path', '/tmp/ro')
            # b.click('.volume-form .btn-add')

            # b.set_file_autocomplete_val("#create-pod-dialog-volume-1 .pf-v5-c-select", rwdir)
            # b.set_input_text('#create-pod-dialog-volume-1-container-path', '/tmp/rw')

    #     b.click("#create-pod-create-btn")
    #     b.set_val("#containers-containers-filter", "all")
    #     self.waitPodContainer(pod_name, [])

        # container_name = 'test-pod-1-system' if auth else 'test-pod-1'
        # cmd = f"docker run -d --pod {pod_name} --name {container_name} --stop-timeout 0 {IMG_ALPINE} sleep 500"
        # containerId = self.execute(auth, cmd).strip()
        # self.waitPodContainer(pod_name,
        #                       [{"name": container_name, "image": IMG_ALPINE,
        #                         "command": "sleep 500", "state": "Running", "id": containerId}], auth)

        # self.toggleExpandedContainer(container_name)
        # b.click(".pf-m-expanded button:contains('Integration')")
        # if self.machine.image not in ["ubuntu-2204"]:
        #     b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Volumes") + dd',
        #                    f"{rodir} \u2194 /tmp/ro")
        #     b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Volumes") + dd',
        #                    f"{rwdir} \u2194 /tmp/rw")

        # b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Ports") + dd',
        #                '0.0.0.0:6000 \u2192 5000/tcp')
        # b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Ports") + dd',
        #                '127.0.0.1:6001 \u2192 5001/udp')
        # b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Ports") + dd',
        #                ' \u2192 9001/tcp')


        # Create pod as admin
        # if auth:
        #     pod_name = 'testpod2'
        #     b.click("#containers-containers-create-pod-btn")
        #     b.set_input_text("#create-pod-dialog-name", pod_name)
        #     b.click("#create-pod-dialog-owner-user")
        #     b.click("#create-pod-create-btn")

    #         b.set_val("#containers-containers-filter", "all")
    #         self.waitPodContainer(pod_name, [])

    @testlib.skipImage("passthrough log driver not supported", "ubuntu-2204")
    def testLogErrors(self):
        b = self.browser
        container_name = "logissue"
        self.login()

        self.execute(False,
                     f"docker run --log-driver=passthrough --name {container_name} -d {IMG_ALPINE} false </dev/null")
        self.waitContainerRow(container_name)
        self.toggleExpandedContainer(container_name)
        b.click(".pf-m-expanded button:contains('Logs')")
        b.wait_in_text(".pf-m-expanded .pf-v5-c-empty-state__content", "failed to obtain logs for Container")

    @testlib.skipOstree("/lib is read-only")
    def testManifest(self):
        b = self.browser
        m = self.machine
        self.restore_file("/lib/systemd/system/docker.socket", post_restore_action="systemctl daemon-reload")
        m.execute("rm /lib/systemd/system/docker.socket")
        self.login_and_go(None)
        b.wait_in_text("#host-apps .pf-m-current", "Overview")

        # HACK: is_pybridge should also check TEST_SCENARIO
        if self.is_pybridge() or os.getenv('TEST_SCENARIO') == 'pybridge':
            self.assertNotIn("Docker", b.text("#host-apps"))
        else:
            self.assertIn("Docker", b.text("#host-apps"))


if __name__ == '__main__':
    testlib.test_main()
